From aba6205274a7fb8c8a55eaedb8001e3a693ed5f1 Mon Sep 17 00:00:00 2001 From: Gerald Versluis Date: Mon, 22 Mar 2021 21:39:24 +0100 Subject: [PATCH] Develop to Main for 1.1 (#1108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added new TextCaseType to TextCaseConverter (#763) * Added new TextCaseType to TextCaseConverter Added support for first char to upper rest to lower case. * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TextCaseConverter.shared.cs * Added unit tests and fix bug for empty string * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TextCaseConverter.shared.cs * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TextCaseConverter.shared.cs * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TextCaseConverter.shared.cs Co-authored-by: Pedro Jesus Co-authored-by: joacim wall Co-authored-by: Andrei Co-authored-by: Pedro Jesus * Add LocalizedString class for automatic ViewModel string updates (#750) * Add LocalizedString * Update test name * Remove from NETSTANDARD1_0 * LangVersion latest * Revert "LangVersion latest" This reverts commit e22a4c12759c682e810af236e91adcdf2e0c458c. * Not using linq discard * Fix test * Fix test. Add constructor with localizationManager * Add new feature to sample app * Add Dispose() to unsubscribe from localizationManager.PropertyChanged * Fix Sample localization resources generation * Update sample app to change AppResources.Culture * Update LocalizedString to use ObservableObject * Return unsubscribing from localizationManager.PropertyChanged * Add WeakSubscribe * Refactor LocalizedString to use WeakSubscribe * Add test to ensure instance of LocalizedString is disposed * Dont create instance of LocalizationResourceManager * Add null checks to WeakSubscribe * Invoke action with less code null check has already been done * Replace spaces with tabs * Revert "Use Single for getting culture" * added one more unit test Co-authored-by: Pedro Jesus * Add CommandFactory class (#797) * Create CommandHelper.shared.cs * Use CommandHelper in Sample app * Rename to CommandFactory * Replace "is null" with "== null" * Update GravatarImageViewModel.cs * Update SearchViewModel.cs * Move CommandFactory to ObjectModel * Update ConvertExecute and ConvertCanExecute to throw InvalidCommandParameterException * Add tests * Skip test in question * Revert Xamarin.CommunityToolkit.csproj changes * Fix tests * Add tests for AsyncValueCommand * Revert AsyncValueCommandSupport * Revert "Revert AsyncValueCommandSupport" This reverts commit 07a178c6ab761cfa5496c776a18ded9560a2e07a. * Creating AsyncValueCommand using CreateValue * Revert "Creating AsyncValueCommand using CreateValue" This reverts commit 7b325610596e67b8fb3dff6600348f4726f25664. * Add Create overloads without optional parameters for Task to act as tiebreakers * Add different names to "execute" parameters to provide another way to resolve ambiguity * Updated CommandFactory - Add XML Documentation - Separated Command.Factory.Command.shared.cs, Command.Factory.IAsyncCommand.shared.cs and Command.Factory.IAsyncValueCommand.shared.cs - Removed unnecessary CommandFactory.Create methods - Added missing CommandFactory.Create methods - Updated Unit Tests * Updated XML Documentation Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> * Deprecate LocalizationResourceManager.SetCulture (#766) * Deprecate SetCulture * Add EditorBrowsableState.Never * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/LocalizationResourceManager.shared.cs * Fix obsolete warning in tests * Remove unneeded line * Use new LocalizationResourceManager for tests Tests were failing from time to time because the same instance of LRM was used for all of them. And since tests are executed in parallel, this caused some issues * Siplify impicit conversion test to test only conversion itself * Return Invalidate mathod Co-authored-by: Pedro Jesus * ShadowEffect (#725) * https://github.com/xamarin/XamarinCommunityToolkit/issues/461 * Added mac support * Added sample * Updated shadows * Fixed iOS crash * removed pragma * Remove effect when color if default * cleaned code * Implemented LazyView (#796) * Created LazyView and implemented it on TabView * Added Preserve attribute * Update LazyTestView.xaml * Adds Popup Control (#653) * Ported Popup Control from Xamarin/Xamarin.Forms#9616 to Xamarin Community Toolkit * Added missing async/await api * Added popup result sample and fixed uwp/android implementations * Renamed View->Content * Updated styles of sample popups * Updated BorderColor to be a platform specific property for UWP * Moved Android/iOS/UWP renderers to platform specific folders * Changed PopupDismissedEventArgs so it is immutable * Removed visual studio edits to sample android project * Removed = false for isDisposed as it is default * Normalized accessor properties for ios and uwp * Removed performance apis since they are used internally at Xamarin.Forms * Simplified OnElementChanged to use shared method ConfigureControl * Removed iOS 9 code * Updated NavigationExtensions to follow Xamarin Essentials style by adding netstandard impl that throws exception * Added Control.Cleanup invocation to dispose method * [iOS] Removed async/await from OnDismissed since it is an event the invocation doesn't need to know when it is complete * Fixed comment * Updated iOS to use WeakEventManager * changed instantiation to use default(SizeRequest) * ios - Changed WeakEventManager so it is a protected static object on the popup renderer. This will help create a weak reference to the popup delegate for the GC * Moved sample popup code to new location * Added WeakEventManager for BasePopup events (Opened, Dismissed) * ios - changed popover delegate to use action * Added PopOverDelegate.PopoverDismissed * Updated accessor for LightDismiss to protected internal * Updated comment in LightDismiss * Removed stale iOS code * Updated xml comments * Removed obj dir from compilation in 'netstandard' projects and fixed debugger issues * removed Init method and added code to get the Context * changed accessor of ToolkitPlatform to internal * Added new UI popup tests for Xaml binding vs C# binding * Updated XCT xmlns to use uri instead of fully qualified namespace * Added shared popup size across samples * Fixed popup BindingContext not working when set in XAML * Update Samples - Capitalize x:Name variables - Replace expression-bodied properties with read-only properties where applicable * Add PopupDismissedEventArgs * Add Default Values for Color and Size * Remove Unncessary `base` Keyword, per styling guide * Optimize originX + originX, Add missing `await` * Update PopupAnchorViewModel.cs * Fix orignY Copy/paste error * Update PopupRenderer.ios.cs * Update NavigationExtensions.netstandard.macos.tvos.watchos.tizen.wpf.gtk.cs * Added NavigableElementExtension - This allows rendering popup from ContentPage and other children of NavigableElement Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Co-authored-by: Pedro Jesus * Update BaseGalleryViewModel.cs * Merging is hard * Fix Anchor Popup Sample NRE (#834) * Changed LoadView to be Async (#828) * Changed LoadView to ValueTask LoadViewAsync and added summaries * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Views/LazyView/LazyView.shared.cs * Popup out of the screen horizontally in Android sample (#839) * Fix popup out of horizontal bounds https://github.com/xamarin/XamarinCommunityToolkit/pull/653#issuecomment-757399625 * Device Size dependent * Essentials namespace * revert * [Android] Converting dimensions to pixels in popup renderer (#856) * [Android] Converting dimensions to pixels in popup renderer * Add fixed and relative to screen sizes instead of platform specific sizes. * Update ButtonPopup size. * Update CsharpBindingPopup size. * Update MultipleButtonPopup size. * Update NoLightDismissPopup size. * Update OpenedEventSimplePopup size. * Update ReturnResultPopup size. * Update SimplePopup size. * Update ToggleSizePopup size. * Update TransparentPopup size. * Update XamlBindingPopup size. * #704 [Bug] No padding around text in a Toast or Snackbar (#714) * Make ShieldView more customizable (#874) * Add SubjectBackgroundColor, SubjectTextColor/reorganization. * Update sample. * Obsolete message * Correct typo in comment Co-authored-by: Gerald Versluis * Drop UpdateTextColor() Co-authored-by: Gerald Versluis * fixed csproj to produce sourcelink (#893) * [iOS] Remove popup arrow if anchor isn't set (#854) * Remove popup arrow if anchor isn't set * Create PopoverArrowDirection.cs * Create Popup.shared.cs * Add arrow direction platform specific. * Add arrow direction platform specific sample. * Rename PopoverArrowDirection.cs to PopoverArrowDirection.shared.cs * Change ArrowDirection to up * Remove switch PopoverArrowDirection.None since default is already zero. * Use div 2 instead of multiply by 0.5 for consistency. Co-authored-by: Sebastian Klatte * ProgressBar Attached Property to enable progress animation (#352) * ProgressBar Attached Property to enable progress animation * Fixes remarks on PR * Fix remarks PR * Changes need for new gallery viewmodel * Correct version from before rebase * Update AttachedPropertiesGalleryViewModel.cs * Update ProgressBarAttachedProperties.shared.cs * Update ProgressBarAttachedProperties.shared.cs * Change Attached property to behavior * Update samples/XCT.Sample/Xamarin.CommunityToolkit.Sample.csproj Co-authored-by: Andrei * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/ProgressBarAnimationBehavior.cs Co-authored-by: Andrei * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Xamarin.CommunityToolkit.csproj Co-authored-by: Andrei * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/ProgressBarAnimationBehavior.cs Co-authored-by: Cfun <15718354+Cfun1@users.noreply.github.com> * Update src/CommunityToolkit/Xamarin.CommunityToolkit/AttachedProperties/ProgressBarAttachedProperties.shared.cs Co-authored-by: Cfun <15718354+Cfun1@users.noreply.github.com> * Remove old attached property and rename new behavior Co-authored-by: Gerald Versluis Co-authored-by: Andrei Co-authored-by: Cfun <15718354+Cfun1@users.noreply.github.com> * Merge main into dev (#913) * [Android] Fix Long Press blocks CollectionView selection (#764) * https://github.com/xamarin/XamarinCommunityToolkit/issues/760 * Moved helper methods to VisualElementExtensions * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/VisualElementExtension.shared.cs * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/VisualElementExtension.shared.cs Co-authored-by: Javier Suárez Co-authored-by: Gerald Versluis * [Android] Fix SideMenuView + Slider issue (#824) * [Android] Allow using slider inside SideMenu with disabled gestures https://github.com/xamarin/XamarinCommunityToolkit/issues/810 * Refactored gesture default threshold https://github.com/xamarin/XamarinCommunityToolkit/issues/810 * Cleaned code. Added linker safety code * Fixed build Co-authored-by: Javier Suárez * Automated dotnet-format update (#837) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Remove `Device` dependency from command implementation (#825) (#830) * Remove `Device` dependency from command implementation (#804) * Move Xamarin.CommunityToolkit.ObjectModel.Internals classes to `Internals` folder * Add Platform-Specific MainThread Implementations, Update Unit Tests to Acommidate Context Switching * Update BaseCommand.uwp.cs * Add BaseCommand.wpf.cs * Finish BaseCommand.gtk.cs * Update BaseCommand.shared.cs Co-authored-by: Eugen Richter Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> * Automated dotnet-format update (#865) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Fixes https://github.com/xamarin/XamarinCommunityToolkit/issues/859 (#864) Co-authored-by: Gerald Versluis * ValidationBehavior: Added IsNotValid property (#836) * Added IsNotValid property to validators * small fix * Updated sample * https://github.com/xamarin/XamarinCommunityToolkit/issues/822 (#877) * Fixes AvatarView - Valid source images dont load for the first time they are created (#849) * https://github.com/xamarin/XamarinCommunityToolkit/issues/805 * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/AvatarView.shared.cs Co-authored-by: Gerald Versluis * Fixed image loading * Fixed null ref * Fixes crashes Co-authored-by: Gerald Versluis Co-authored-by: Javier Suárez Co-authored-by: Pedro Jesus * Added inline docs (#882) * Update ViewsGalleryViewModel.cs (#888) removed duplicated text "used to" * Document all behaviors and associated (#895) * Housekeeping remove XamlCompilation from code-behind. (#897) * Changed TabView SelectedIndex property to use TwoWay binding mode (#903) * Add inline Docs/Comments to Converters (#907) * Update azure-pipelines.yml Co-authored-by: Andrei Co-authored-by: Javier Suárez Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Eugen Richter Co-authored-by: Eugen Richter Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Co-authored-by: Pedro Jesus Co-authored-by: WillAutioItrax <15933820+WillAutioItrax@users.noreply.github.com> Co-authored-by: Cfun <15718354+Cfun1@users.noreply.github.com> * Add "Add(IEnumerable collection)" to ObservableRangeCollection (#891) * Create ObservableRangeCollectionEx.shared.cs * Add test * Update src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Extensions/ObservableRangeCollectionEx.shared.cs * Resolve comments * Add AddToNullCollection test * Fix WeakSubscribe parameter type * Revert "Fix WeakSubscribe parameter type" This reverts commit 221ee0fe2003c548971e5aca36b54e3e5f43b0d0. Co-authored-by: Andrei * Update INotifyPropertyChangedEx.shared.cs (#918) * [Breaking Changes] ValidationBehavior Async checks support (#912) * Refactored validation behavior * Refactored to ValueTask * added configure await false * Fixed isRunning * LifeCycleEvents (#674) * Created LifeCycleEffect and platform implementations * Added sample to LifeCycle effect * refactor UnLoaded logic * clean up csproj * Fixed NRE for shell * Code Review fixes * code style * improved the sample * fixed tizen path * Fixed iOS support * Implemented WeakEventManager * Added inline docs/comments * changed to nameof * Msbuild hotfix (#946) * fixed msbuild instructions * revert android sample csproj * Add `Grouping` class from Refractored.MvvmHelpers (#957) * Fix LocalizedString gets unsubscribed while still in use (#987) * Fixed all the things * Make PersistentObservableObject abstract * Reverse the inheritance * Commit to rerun tests * Remove PersistentObservableObject * Grammar * Feature/371 enum boolean converter (#925) * Implement EnumToBoolConverter (#371) * Add example for EnumToBoolConverter * Make it work with flagged enums (#371) * Simplify code (#371) * Make the method an local static method (#371) * Cache enum type info (#371) Co-authored-by: Eugen Richter * Add VSM to ValidationBehavior (#955) * Add VSM to ValidationBehavior * dumb commit: Re trigger CI * Fix typo * Fix typo Co-authored-by: Andrei * Change string properties to public * Apply Pascal Case Naming * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/ValidationBehavior.shared.cs * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/ValidationBehavior.shared.cs * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/ValidationBehavior.shared.cs * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/ValidationBehavior.shared.cs * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/ValidationBehavior.shared.cs Co-authored-by: Andrei * Fix Singleton Pattern for LocalizationResourceManager (#919) * Update LocalizationResourceManager.shared.cs * Implement Lazy Loading for Thread Safety * Fix Namespace * Removed changes from main branch * Revert Commit * Update LocalizationResourceManager.shared.cs * Fix Unit Tests * Add Collection to LocalizationResourceManagerTests The `Collection` attribute prevents these tests from running in parallel https://xunit.net/docs/running-tests-in-parallel.html * [Enhancement] Add Scale support for SideMenuView (#1011) * SideMenu scaling https://github.com/xamarin/XamarinCommunityToolkit/issues/1007 * Added easing * Update azure-pipelines.yml * [iOS] Fix missing binding context for popup view (#1003) * [iOS] Fix missing binding context for popup view * Fix handle binding context changes. * Enable Nullable (#1009) * Enable Nullable on Unit Tests (#1008) * Enable Nullability on Xamarin.CommunityToolkit.Markup (#1013) * Enable Nullable on Unit Tests * Enable Nullable on Xamarin.CommunityToolkit.Markup * Enable Nullability on Xamarin.CommunityToolkit.Sample (#1014) * Enable Nullable on Unit Tests * Enable Nullable on Xamarin.CommunityToolkit.Markup * Enable Nullable on Xamarin.CommunityToolkit.Sample * Enable Nullability for Android, GTK, iOS, Tizen, UWP & WPF Sample Projects (#1015) * Enable Nullable on Unit Tests * Enable Nullable on Xamarin.CommunityToolkit.Markup * Enable Nullable on Xamarin.CommunityToolkit.Sample * Enable Nullable on Android, GTK, iOS and Tizen Samples * Enable Nullable for UWP & WPF Sample Projects * Enable Nullability on Xamarin.CommunityToolkit (#1016) * Enable Nullable on Unit Tests * Enable Nullable on Xamarin.CommunityToolkit.Markup * Enable Nullable on Xamarin.CommunityToolkit.Sample * Enable Nullable on Android, GTK, iOS and Tizen Samples * Enable Nullable for UWP & WPF Sample Projects * Add Nullability * Enable Nullable on XamarinCommunityToolkit (#1023) * Enable Nullable on Unit Tests * Enable Nullable on Xamarin.CommunityToolkit.Markup * Enable Nullable on Xamarin.CommunityToolkit.Sample * Enable Nullable on Android, GTK, iOS and Tizen Samples * Enable Nullable for UWP & WPF Sample Projects * Add Nullability * Resolve Possible Null References * Removed Possible Null References * Update AppResources.Designer.cs * Handle Nullability * Updated Nullabiltiy * Update Converters & Unit Tests * Resolve MediaSource Unit Tests * Fix Unit Tests (#1036) * Enable Nullable on Unit Tests * Enable Nullable on Xamarin.CommunityToolkit.Markup * Enable Nullable on Xamarin.CommunityToolkit.Sample * Enable Nullable on Android, GTK, iOS and Tizen Samples * Enable Nullable for UWP & WPF Sample Projects * Add Nullability * Resolve Possible Null References * Removed Possible Null References * Update AppResources.Designer.cs * Handle Nullability * Updated Nullabiltiy * Update Converters & Unit Tests * Resolve MediaSource Unit Tests * Fix VariableMultiValueConverter * Fixed ImpliedOrderGridBehavior * Update NumericValidationBehavior.shared.cs * Resolve Nullable in SideMenuView * Move enable to Directory.Build.props * Update Xamarin.CommunityToolkit.Sample.csproj * Revert Designer.cs * Update Xamarin.CommunityToolkit.Sample.csproj * Update ItemSelectedEventArgsConverter_Tests.cs * Update SearchViewModel.cs * Update ItemTappedEventArgsConverter_Tests.cs * Update Xamarin.CommunityToolkit.UnitTests.csproj * Add Nullability * Resolve Compiler Warnings * Ignore Closing square brackets should be spaced correctly With Nullable enabled, `byte[]?` is now valid, however SA1011 was still generating a warning * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/ImpliedOrderGridBehavior.shared.cs Co-authored-by: Pedro Jesus * Update CameraFragment.android.cs * Update CameraFragment.android.cs * Update ImpliedOrderGridBehavior.shared.cs * Update MaskedBehavior.shared.cs * Update PopupRenderer.uwp.cs * Use .NET 5.0 * Update azure-pipelines.yml * Update azure-pipelines.yml * Update azure-pipelines.yml * Add UWP to Release Build * Update Nullability * Update Nullablity * Update TabView.shared.cs * Update Nullability * Revert "Update Nullability" This reverts commit e391b9c654e7b6b2c55d38dd9dcfffa8b5232403. * Resolved Nullable * Update azure-pipelines.yml * Revert UWP Build Properties * Update azure-pipelines.yml * Update azure-pipelines.yml * Revert "Update azure-pipelines.yml" This reverts commit 0842280e362f64df26a8cff2e028fc77d17b48cb. * Update azure-pipelines.yml * Revert "Revert UWP Build Properties" This reverts commit 77226bfd189530f67fd2f18cac1a22eedb20113b. * Update azure-pipelines.yml * Update azure-pipelines.yml * Update azure-pipelines.yml * Update azure-pipelines.yml * Update azure-pipelines.yml * Update azure-pipelines.yml * Revert "Update azure-pipelines.yml" This reverts commit 4eb36f41269882f6bc25a0e96bc9647f756f4a68. * Update azure-pipelines.yml * Revert "Update azure-pipelines.yml" This reverts commit e41a477dc4c1dc79e6810f6d8a8bc1381176fe95. * Fix ValidationBehavior.ForceValidate and ValidationBehavior.DefaultForceValidateCommand * Update SelectAllTextEffect.ios.cs * Remove Nullabilty from LocalizedString.generator * Update MediaElementRenderer.ios.cs * Update PopupRenderer.uwp.cs * Update PopupRenderer.ios.cs * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackbarViews/BaseSnackBarView.ios.cs Co-authored-by: Maksym Koshovyi * Update TranslateExtension.shared.cs * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.android.cs Co-authored-by: Maksym Koshovyi * Update Logic * Fix Failing ICommand Tests * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.tizen.cs Co-authored-by: Maksym Koshovyi * Throw Faulted Task * Use Cast instead of Pattern Matching * Update RangeSlider.shared.cs * Fix missing semi-colons * Throw InvalidOperationException in LocalizationResourceManager * Make TranslateExtension.StringFormat nullable * Update UserStoppedTypingBehavior.shared.cs * Update SearchPage.logic.cs * Update TouchEffectCollectionViewPage.xaml.cs * Update AppResources.Designer.cs * Update AppResources.Designer.cs * Update EnumToBoolConverterViewModel.cs * Update EnumToBoolConverter_Tests.cs * Update IntToBoolConverter_Tests.cs * Update InvertedBoolConverter_Tests.cs * Update IsNotNullOrEmptyConverter_Tests.cs * Update MultiConverter_Tests.cs * Update NotEqualConverter_Tests.cs * Update TextCaseConverter_Tests.cs * Update MockPlatformServices.cs * Update MockPlatformServices.cs * Update Namespace_Tests.cs * Update ObservableRangeCollection_Tests.cs * Update ObservableRangeCollection_Tests.cs * Use `async Task` instead of `async void` * Update MultiValidationBehavior.shared.cs * Update EnumToBoolConverter.shared.cs * Update EnumToBoolConverter.shared.cs * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/NotEqualConverter.shared.cs Co-authored-by: Pedro Jesus * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/StateToBooleanConverter.shared.cs Co-authored-by: Pedro Jesus * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TextCaseConverter.shared.cs Co-authored-by: Pedro Jesus * Update IconTintColorEffectRouter.android.cs * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SelectAllText/SelectAllTextEffect.android.cs Co-authored-by: Pedro Jesus * Update SelectAllTextEffect.android.cs * Update SelectAllTextEffect.ios.cs * Update PlatformTouchEffect.ios.cs * Update PlatformTouchEffect.uwp.cs * Update PlatformTouchEffect.uwp.cs * Update PlatformTouchEffect.macos.cs * Update PlatformTouchEffect.uwp.cs * Update PlatformTouchEffect.uwp.cs * Ensure nullable results from BindableProperties are still resolved * Update ImageResourceExtension.shared.cs * Update BaseCommand.android.cs * Use protected private constructor for BaseCommand * Update BadgeView.shared.cs * Update CameraFragment.android.cs * Fix Android Media Bugs * Update async/await * Update CameraView_Tests.cs * Update CameraView * Update UriTypeConverter.shared.cs * Update PopupRenderer.uwp.cs * Update PopoverArrowDirection.shared.cs * Update PopoverArrowDirection.shared.cs * Update TabView.shared.cs * Improve AsyncCommand Tests * Ensure Context is non nullable * Remove Missing Translations * Fix async/await in TouchEffect.shared.cs * Make Easing Nullable * Update Samples * Fix Null Exception * Resolve NullReferenceExceptions * Make IBadgeAnimation Nullable * Add ShutterCommandValueCreator null check * Add Timeout to prevent race conditions from stalling tests * Unsubscribe Event Handlers for AsyncCommand Tests * Update azure-pipelines.yml * For WPF, Use Cross-Platform Implementation for `BaseCommand.IsMainThread` and `BaseCommand.BeginInvokeOnMainThread` (#965) * Add Non-WPF Support to .NET Core 3.1 * Update comments * Merge .NET Standard and WPF Functionality * Fix Null Reference * Update ICommand_AsyncValueCommand_Tests.cs * Add volatile keyword * Update AsyncValueCommand_Tests.cs * Remove Timeouts * Fix Event Unsubscription * Add .ConfigureAwait(false); * Run dotnet test serially * Use SemaphoreSlim to prevent Race Conditions Co-authored-by: Pedro Jesus Co-authored-by: Maksym Koshovyi * add SetFocusOnEntryCompleted (#841) (#911) * add SetFocusOnEntryCompleted (#841) * prevent SetFocusOnEntryCompleted memory leak (#841) also force exception if used on non-Entry * cleanup (reuse variable) (#841) * change to attached behavior SetFocusOnEntryCompletedBehavior * remove AttachedBehaviors sub-namespace * fix SetFocusOnEntryCompleted_Tests * simplify SetFocusOnEntryCompletedBehaviorPage * fixed code to be nullable safe * code style Co-authored-by: Pedro Jesus * https://github.com/xamarin/XamarinCommunityToolkit/issues/1046 (#1052) * #1025 (#1042) * Merge fixing * Implement SafeFireAndForgetExtensions (#1054) * Implement Safe-Fire-And-Forget * Change `public` to `internal` * Update PlatformTouchEffect.android.cs * Regression #853 (#1063) * [iOS] Fix popup default color and dark mode (#1041) * [iOS] Fix popup default color and dark mode * [Android] Set correct background for default color * [Android] GetWindowColor() for popup background color * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/Android/PopupRenderer.android.cs * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/Android/PopupRenderer.android.cs * Update BasePopup.shared.cs * Update PopupRenderer.android.cs * Update PopupRenderer.android.cs * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/BasePopup.shared.cs Co-authored-by: Andrei Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> * Add remove border effect implementation for uwp (#1048) Co-authored-by: Alexander Witkowski * Added long press event https://github.com/xamarin/XamarinCommunityToolkit/issues/851 (#1064) * Update LocalizedString.shared.cs (#1071) * Image resource converter (#1068) * Implement ImageResourceConverter * Add a sample for ImageResource extension * Fix nullable errors * Rename converter file to be compiled. * Better exception message Co-authored-by: Andrei * Update samples/XCT.Sample/ViewModels/Converters/ImageResourceConverterViewModel.cs * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ImageResourceConverter.shared.cs * Update samples/XCT.Sample/ViewModels/Converters/ImageResourceConverterViewModel.cs * Update samples/XCT.Sample/ViewModels/Converters/ImageResourceConverterViewModel.cs * Update samples/XCT.Sample/ViewModels/Converters/ImageResourceConverterViewModel.cs * Fix GalleryPage and VM * Update samples/XCT.Sample/ViewModels/Extensions/ExtensionsGalleryViewModel.cs Co-authored-by: Andrei * Update samples/XCT.Sample/ViewModels/Extensions/ExtensionsGalleryViewModel.cs Co-authored-by: Andrei * Replace Exception approach by extension used by XF (#1028) * Replace Exception approach by extension used by XF * Simplified syntax as requested * Add a missing namespace * Resolve `IAsyncCommand` / `IAsyncValueValueCommand` Unit Test Race Condition (#1076) * Add Unit Test Timeouts * Add macOS Unit Tests * Build 'samples/XCT.Sample.sln' on macOS * Restore Unit Test NuGet Packages * Fix YAML formatting * Add xunit.runner.console * Use latest stable version of Microsoft.NET.Test.Sdk * Fix Unit Tests on macos * Update azure-pipelines.yml * Update azure-pipelines.yml * Update azure-pipelines.yml * Add SemaphoreSlim * Add `dotnet restore` * Update azure-pipelines.yml * Update azure-pipelines.yml * Revert "Add SemaphoreSlim" This reverts commit 687586551e7b6071d10411130d35175b8a886434. * Revert BaseCommand.semaphoreSlim * Update azure-pipelines.yml * Update azure-pipelines.yml * Update azure-pipelines.yml * Add ConfigureAwait(false) to ValueTaskDelay(int) * Change Platform from NetCore to macOS * Passthrough ValueTask * Update azure-pipelines.yml * Revert "Update azure-pipelines.yml" This reverts commit d7b284243facc84a5cffff1c3bc86887e7af19f8. * Use ConfigureAwait(false) * Rename Task * Remove `static` * Remove `static` * Revert p:BuildInParallel=false * Update azure-pipelines.yml * Revert "Update azure-pipelines.yml" This reverts commit bf93f0ce35788a4d566bab2f35b2a2b53edcfa7c. * Revert "Revert "Update azure-pipelines.yml"" This reverts commit 031121a87340ffd1679b8ec3a7af0dcfb29b3967. * Revert "Revert "Revert "Update azure-pipelines.yml""" This reverts commit 458feb25200794080d2b2d723ce7b3fe43625978. * https://github.com/xamarin/XamarinCommunityToolkit/issues/1066 (#1078) Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> * fix badge background color (#1080) (#1081) Co-authored-by: Matthew S Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Co-authored-by: Pedro Jesus * allow TextCaseConverter to handle any value type (#1053) * allow TextCaseConverter to handle any value type * remove duplicate Convert method * fix nullability * update TextCaseConverter tests * Update src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TextCaseConverter.shared.cs Co-authored-by: Andrei Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Co-authored-by: Pedro Jesus * https://github.com/xamarin/XamarinCommunityToolkit/issues/850 (#1056) Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Co-authored-by: Pedro Jesus * Platform specific API (#949) * added iOS platformSpecific * added UWP platform specific * Changed the sample * code clean up * added null check for Element Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Co-authored-by: Gerald Versluis * Add some null checks (#1033) Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Co-authored-by: Javier Suárez Co-authored-by: Gerald Versluis * Migrate Unit Tests to NUnit (#1090) * Migrate to NUnit * Update Tests * Update Azure Pipelines * Update TextCaseConverter_Tests.cs * Update DateTimeOffsetConverter_Tests.cs * Increase Timeout * Increase Timeout * Add `const int defaultTimeoutThreshold` Co-authored-by: Gerald Versluis * Enable Nullable for Xamarin.CommunityToolkit.Markup.UnitTests (#1086) * Add Markup Unit Tests * Add Nullable to Xamarin.CommunityToolkit.Markdown.UnitTests * Update BindingHelpers.cs * Fix Failing Unit Tests * Don't capture XUnit's Synchronization Context * Re-order Unit Tests * Revert "Don't capture XUnit's Synchronization Context" This reverts commit b8b3f16ec466b848e83387c26b3e08a86028613b. Co-authored-by: Javier Suárez * Implement a generic version of the EventToCommandBehavior (#1010) * created ECBGeneric * Added Generic implemention to the EventToCommandBehavior * make EventToCommandBehavior sealed * Fixed for value types * added nullable notations * update the comment * fixed unit test * Migrate Unit Tests to NUnit * Fixed unitTest * changed to use C#9 features * removed Convert method * Add Inheritance Test Co-authored-by: Gerald Versluis Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> * [UWP] TouchEffect OnPointerReleased crash (#1088) * Fixed TouchEffect UWP crash * Touch Effect more nullRef fixes * fixed build * fix build Co-authored-by: Javier Suárez Co-authored-by: Pedro Jesus Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Co-authored-by: Joacim Wall Co-authored-by: joacim wall Co-authored-by: Andrei Co-authored-by: Pedro Jesus Co-authored-by: Maksym Koshovyi Co-authored-by: Brandon Minnick <13558917+brminnick@users.noreply.github.com> Co-authored-by: Andrew Hoefling Co-authored-by: Cfun <15718354+Cfun1@users.noreply.github.com> Co-authored-by: Sebastian Klatte Co-authored-by: Vladislav Antonyuk <33021114+VladislavAntonyuk@users.noreply.github.com> Co-authored-by: Sebastian Klatte Co-authored-by: Glenn Versweyveld Co-authored-by: Javier Suárez Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Eugen Richter Co-authored-by: Eugen Richter Co-authored-by: WillAutioItrax <15933820+WillAutioItrax@users.noreply.github.com> Co-authored-by: tranb3r Co-authored-by: Jacob Egner Co-authored-by: Alexander Witkowski Co-authored-by: Alexander Witkowski Co-authored-by: Matthew S Co-authored-by: Matthew S Co-authored-by: Dan Siegel --- .editorconfig | 6 + Directory.Build.props | 5 +- Xamarin.CommunityToolkit.ruleset | 1 - azure-pipelines.yml | 89 ++- samples/XCT.Sample.Android/MainActivity.cs | 5 +- samples/XCT.Sample.Android/SplashActivity.cs | 2 +- ...rin.CommunityToolkit.Sample.Android.csproj | 2 +- samples/XCT.Sample.UWP/App.xaml.cs | 4 +- .../Renderers/NoLineNavigationRenderer.cs | 7 +- samples/XCT.Sample.sln | 36 +- samples/XCT.Sample/App.xaml.cs | 6 +- samples/XCT.Sample/Helpers/RelayCommand.cs | 84 ++- samples/XCT.Sample/Helpers/XLog.cs | 26 +- samples/XCT.Sample/Images/logo.png | Bin 0 -> 4630 bytes samples/XCT.Sample/Pages/AboutPage.xaml | 6 +- samples/XCT.Sample/Pages/AboutPage.xaml.cs | 9 +- .../Pages/Base/BaseNavigationPage.cs | 1 + samples/XCT.Sample/Pages/Base/BasePage.cs | 21 +- .../Behaviors/AnimationBehaviorPage.xaml | 2 +- .../Behaviors/AnimationBehaviorPage.xaml.cs | 3 +- .../CharactersValidationBehaviorPage.xaml | 92 ++- .../ImpliedOrderGridBehaviorLabel.cs | 2 +- .../ImpliedOrderGridBehaviorPage.xaml.cs | 8 +- .../MaxLengthReachedBehaviorPage.xaml.cs | 2 +- .../ProgressBarAnimationBehaviorPage.xaml | 26 + .../ProgressBarAnimationBehaviorPage.xaml.cs | 10 + .../SetFocusOnEntryCompletedBehaviorPage.xaml | 34 + ...tFocusOnEntryCompletedBehaviorPage.xaml.cs | 8 + .../BoolToObjectConverterPage.xaml.cs | 14 +- .../ByteArrayToImageSourcePage.xaml | 4 + .../ByteArrayToImageSourcePage.xaml.cs | 10 +- .../DoubleToIntConverterPage.xaml.cs | 14 +- .../Converters/EnumToBoolConverterPage.xaml | 58 ++ .../EnumToBoolConverterPage.xaml.cs | 7 + .../Converters/EqualConverterPage.xaml.cs | 14 +- .../ImageResourceConverterPage.xaml | 24 + .../ImageResourceConverterPage.xaml.cs | 8 + .../IndexToArrayItemConverterPage.xaml.cs | 14 +- .../Converters/IntToBoolConverterPage.xaml.cs | 14 +- .../InvertedBoolConverterPage.xaml.cs | 7 +- .../IsNotNullOrEmptyConverterPage.xaml.cs | 14 +- .../ListToStringConverterPage.xaml.cs | 14 +- .../Converters/NotEqualConverterPage.xaml.cs | 14 +- .../Converters/TextCaseConverterPage.xaml.cs | 7 +- .../Effects/IconTintColorEffectPage.xaml.cs | 3 +- .../Pages/Effects/LifeCycleEffectPage.xaml | 38 ++ .../Pages/Effects/LifeCycleEffectPage.xaml.cs | 54 ++ .../Pages/Effects/SafeAreaEffectPage.xaml.cs | 2 +- .../Pages/Effects/ShadowEffectPage.xaml | 77 +++ .../Pages/Effects/ShadowEffectPage.xaml.cs | 7 + .../Pages/Effects/TouchEffectPage.xaml | 3 +- .../Pages/Effects/TouchEffectPage.xaml.cs | 8 +- .../Extensions/ExtensionsGalleryPage.xaml | 17 +- .../Extensions/ExtensionsGalleryPage.xaml.cs | 3 +- .../ImageResourceExtensionPage.xaml | 17 + .../ImageResourceExtensionPage.xaml.cs | 7 + .../Pages/Markup/SearchPage.logic.cs | 11 +- samples/XCT.Sample/Pages/SettingPage.xaml | 4 +- samples/XCT.Sample/Pages/SettingPage.xaml.cs | 3 +- .../TestCases/MediaElementSourcePage.xaml.cs | 16 +- .../TestCases/TouchEffectButtonPage.xaml.cs | 11 +- .../TouchEffectCollectionViewPage.xaml.cs | 17 +- .../Pages/Views/AvatarViewPage.xaml | 2 +- .../XCT.Sample/Pages/Views/BadgeViewPage.xaml | 2 +- .../Pages/Views/CameraViewPage.xaml.cs | 14 +- .../Pages/Views/MediaElementPage.xaml.cs | 10 +- .../Pages/Views/PopupGalleryPage.xaml | 66 ++ .../Pages/Views/PopupGalleryPage.xaml.cs | 7 + .../Pages/Views/Popups/ButtonPopup.xaml | 47 ++ .../Pages/Views/Popups/ButtonPopup.xaml.cs | 9 + .../Views/Popups/CsharpBindingPopup.xaml | 41 ++ .../Views/Popups/CsharpBindingPopup.xaml.cs | 13 + .../Views/Popups/MultipleButtonPopup.xaml | 61 ++ .../Views/Popups/MultipleButtonPopup.xaml.cs | 11 + .../Views/Popups/NoLightDismissPopup.xaml | 51 ++ .../Views/Popups/NoLightDismissPopup.xaml.cs | 9 + .../Views/Popups/OpenedEventSimplePopup.xaml | 41 ++ .../Popups/OpenedEventSimplePopup.xaml.cs | 21 + .../Pages/Views/Popups/PopupAnchorPage.xaml | 53 ++ .../Views/Popups/PopupAnchorPage.xaml.cs | 35 + .../Pages/Views/Popups/PopupPositionPage.xaml | 117 ++++ .../Views/Popups/PopupPositionPage.xaml.cs | 7 + .../Pages/Views/Popups/PopupSize.cs | 18 + .../Pages/Views/Popups/ReturnResultPopup.xaml | 48 ++ .../Views/Popups/ReturnResultPopup.xaml.cs | 12 + .../Pages/Views/Popups/SimplePopup.xaml | 41 ++ .../Pages/Views/Popups/SimplePopup.xaml.cs | 7 + .../Pages/Views/Popups/ToggleSizePopup.xaml | 47 ++ .../Views/Popups/ToggleSizePopup.xaml.cs | 27 + .../Pages/Views/Popups/TransparentPopup.xaml | 21 + .../Views/Popups/TransparentPopup.xaml.cs | 18 + .../Pages/Views/Popups/XamlBindingPopup.xaml | 46 ++ .../Views/Popups/XamlBindingPopup.xaml.cs | 7 + .../Pages/Views/RangeSliderPage.xaml.cs | 6 +- .../XCT.Sample/Pages/Views/ShieldPage.xaml | 136 ++-- .../XCT.Sample/Pages/Views/ShieldPage.xaml.cs | 2 +- .../Pages/Views/SideMenuViewPage.xaml | 1 + .../Pages/Views/SideMenuViewPage.xaml.cs | 4 +- .../XCT.Sample/Pages/Views/SnackBarPage.xaml | 3 +- .../Pages/Views/SnackBarPage.xaml.cs | 91 +-- .../Views/TabView/CustomTabsPage.xaml.cs | 2 +- .../Pages/Views/TabView/LazyTabPage.xaml | 57 ++ .../Pages/Views/TabView/LazyTabPage.xaml.cs | 7 + .../Pages/Views/TabView/LazyTestView.xaml | 17 + .../Pages/Views/TabView/LazyTestView.xaml.cs | 29 + .../Pages/Views/TabView/NormalTestView.xaml | 11 + .../Views/TabView/NormalTestView.xaml.cs | 16 + .../Pages/Views/TabView/TabBadgePage.xaml.cs | 4 +- .../Views/TabView/TabPlacementPage.xaml.cs | 2 +- samples/XCT.Sample/Pages/WelcomePage.xaml.cs | 4 +- .../XCT.Sample/Resx/AppResources.Designer.cs | 72 +- samples/XCT.Sample/Resx/AppResources.en.resx | 135 ++++ .../Resx/AppResources.es.Designer.cs | 74 --- samples/XCT.Sample/Resx/AppResources.es.resx | 5 +- samples/XCT.Sample/Resx/AppResources.resx | 125 ++-- .../XCT.Sample/ViewModels/AboutViewModel.cs | 28 +- .../ViewModels/Base/BaseGalleryViewModel.cs | 15 +- .../Behaviors/BehaviorsGalleryViewModel.cs | 8 + .../EventToCommandBehaviorViewModel.cs | 9 +- .../MaxLengthReachedBehaviorViewModel.cs | 6 +- .../ProgressBarAnimationBehaviorViewModel.cs | 31 + .../UserStoppedTypingBehaviorViewModel.cs | 10 +- .../ByteArrayToImageSourceViewModel.cs | 11 +- .../Converters/ConvertersGalleryViewModel.cs | 8 + .../EnumToBoolConverterViewModel.cs | 25 + .../ImageResourceConverterViewModel.cs | 41 ++ .../IndexToArrayItemConverterViewModel.cs | 4 +- .../IsNullOrEmptyConverterViewModel.cs | 14 +- .../ItemSelectedEventArgsViewModel.cs | 4 +- .../ItemTappedEventArgsViewModel.cs | 19 +- .../Converters/ListIsNullOrEmptyViewModel.cs | 7 +- .../VariableMultiValueConverterViewModel.cs | 3 +- .../Effects/EffectsGalleryViewModel.cs | 10 + .../Extensions/ExtensionsGalleryViewModel.cs | 18 + .../ViewModels/Markup/SearchViewModel.cs | 28 +- .../XCT.Sample/ViewModels/SettingViewModel.cs | 35 +- .../ViewModels/Views/BadgeViewViewModel.cs | 15 +- .../ViewModels/Views/ExpanderViewModel.cs | 24 +- .../ViewModels/Views/PopupControlViewModel.cs | 61 ++ .../Popups/CsharpBindingPopupViewModel.cs | 9 + .../Views/Popups/PopupAnchorViewModel.cs | 26 + .../Views/Popups/PopupPositionViewModel.cs | 79 +++ .../Views/Popups/XamlBindingPopupViewModel.cs | 9 + .../ViewModels/Views/StateLayoutViewModel.cs | 23 +- .../Views/TabItemsSourceViewModel.cs | 213 +++--- .../ViewModels/Views/TabViewViewModel.cs | 5 +- .../Views/Tabs/LazyTestViewModel.cs | 27 + .../Views/Tabs/NormalTestViewModel.cs | 17 + .../ViewModels/Views/ViewsGalleryViewModel.cs | 5 +- .../Xamarin.CommunityToolkit.Sample.csproj | 27 +- .../CharactersValidationBehavior_Tests.cs | 74 ++- .../EventToCommandBehaviorGeneric_Tests.cs | 170 +++++ .../Behaviors/EventToCommandBehavior_Tests.cs | 12 +- .../ImpliedOrderGridBehavior_Tests.cs | 16 +- .../MaxLengthReachedBehavior_Tests.cs | 36 +- .../MultiValidationBehavior_Tests.cs | 77 ++- .../NumericValidationBehavior_Tests.cs | 108 +-- .../RequiredStringValidationBehavior_Tests.cs | 53 +- .../SetFocusOnEntryCompleted_Tests.cs | 54 ++ .../Behaviors/UriValidationBehavior_Tests.cs | 32 +- .../UserStoppedTypingBehavior_Tests.cs | 67 +- .../Converters/BoolToObjectConverter_Tests.cs | 19 +- .../ByteArrayToImageSourceConverter_Tests.cs | 30 +- .../DateTimeOffsetConverter_Tests.cs | 35 +- .../Converters/DoubleToIntConverter_Tests.cs | 25 +- .../Converters/EnumToBoolConverter_Tests.cs | 92 +++ .../Converters/EqualConverter_Tests.cs | 18 +- .../IndexToArrayItemConverter_Tests.cs | 21 +- .../Converters/IntToBoolConverter_Tests.cs | 32 +- .../Converters/InvertedBoolConverter_Tests.cs | 18 +- .../IsNotNullOrEmptyConverter_Tests.cs | 15 +- .../IsNullOrEmptyConverter_Tests.cs | 13 +- .../ItemSelectedEventArgsConverter_Tests.cs | 16 +- .../ItemTappedEventArgsConverter_Tests.cs | 16 +- .../ListIsNotNullOrEmptyConverter_Tests.cs | 25 +- .../ListIsNullOrEmptyConverter_Tests.cs | 27 +- .../Converters/ListToStringConverter_Tests.cs | 36 +- .../MultiConverterParameter_Tests.cs | 3 +- .../Converters/MultiConverter_Tests.cs | 18 +- .../Converters/NotEqualConverter_Tests.cs | 15 +- .../Converters/TextCaseConverter_Tests.cs | 55 +- .../VariableMultiValueConverter_Tests.cs | 53 +- .../EnableInitOnlyProperties.cs | 4 + .../LocalizationResourceManagerTests.cs | 52 +- .../LocalizedStringTests.cs | 119 ++++ .../DelegateWeakEventManager_Action_Tests.cs | 30 +- ...DelegateWeakEventManager_Delegate_Tests.cs | 86 ++- ...gateWeakEventManager_EventHandler_Tests.cs | 68 +- .../WeakEventManager_ActionT_Tests.cs | 82 ++- .../WeakEventManager_EventHandlerT_Tests.cs | 98 +-- .../Mocks/MockEnum.cs | 9 + .../Mocks/MockItem.cs | 12 + .../Mocks/MockPlatformServices.cs | 37 +- .../Mocks/MockResourceManager.cs | 2 +- .../Namespace_Tests.cs | 49 +- .../ObjectModel/Grouping_Tests.cs | 57 ++ .../AsyncCommandTests/AsyncCommand_Tests.cs | 276 +++++--- .../AsyncCommandTests/IAsyncCommand_Tests.cs | 102 +-- .../ICommand_AsyncCommand_Tests.cs | 165 +++-- .../AsyncValueCommand_Tests.cs | 231 ++++--- .../BaseAsyncValueCommandTests.cs | 6 +- .../IAsyncValueCommand_Tests.cs | 56 +- .../ICommand_AsyncValueCommand_Tests.cs | 171 +++-- .../ICommandTests/BaseCommandTests.cs | 29 +- .../CommandFactory_AsyncCommand_Tests.cs | 280 ++++++++ .../CommandFactory_AsyncValueCommand_Tests.cs | 279 ++++++++ .../CommandFactory_Command_Tests.cs | 182 ++++++ .../ObjectModel/ObservableObject_Tests.cs | 112 ++-- .../ObservableRangeCollection_Tests.cs | 108 +-- .../ObjectModel/Person.cs | 31 + .../Views/CameraView_Tests.cs | 34 +- .../Views/MediaElement_Tests.cs | 33 +- .../Views/MediaSource_Tests.cs | 123 ++-- .../Views/SnackBar_Tests.cs | 27 +- .../Xamarin.CommunityToolkit.UnitTests.csproj | 8 +- .../Animations/AnimationBehavior.shared.cs | 11 +- .../AnimationTypes/AnimationBase.shared.cs | 2 +- .../AnimationTypes/FadeAnimation.shared.cs | 9 +- .../FlipHorizontalAnimation.shared.cs | 9 +- .../FlipVerticalAnimation.shared.cs | 9 +- .../AnimationTypes/RotateAnimation.shared.cs | 9 +- .../AnimationTypes/ScaleAnimation.shared.cs | 9 +- .../AnimationTypes/ShakeAnimation.shared.cs | 15 +- ...SetFocusOnEntryCompletedBehavior.shared.cs | 50 ++ .../Behaviors/BaseBehavior.shared.cs | 12 +- .../EventToCommandBehavior.shared.cs | 21 +- .../EventToCommandBehaviorGeneric.shared.cs | 28 + .../ImpliedOrderGridBehavior.shared.cs | 14 +- .../Behaviors/MaskedBehavior.shared.cs | 36 +- .../MaxLengthReachedBehavior.shared.cs | 8 +- .../ProgressBarAnimationBehavior.shared.cs | 30 + .../UserStoppedTypingBehavior.shared.cs | 21 +- .../CharactersValidationBehavior.shared.cs | 11 +- .../EmailValidationBehavior.shared.cs | 14 +- .../MultiValidationBehavior.shared.cs | 35 +- .../NumericValidationBehavior.shared.cs | 30 +- ...RequiredStringValidationBehavior.shared.cs | 12 +- .../TextValidationBehavior.shared.cs | 38 +- .../UriValidationBehavior.shared.cs | 6 +- .../Validators/ValidationBehavior.shared.cs | 111 +++- .../BoolToObjectConverter.shared.cs | 8 +- .../ByteArrayToImageSourceConverter.shared.cs | 8 +- .../DateTimeOffsetConverter.shared.cs | 4 +- .../Converters/DoubleToIntConverter.shared.cs | 6 +- .../Converters/EnumToBoolConverter.shared.cs | 65 ++ .../Converters/EqualConverter.shared.cs | 8 +- .../ImageResourceConverter.shared.cs | 40 ++ .../IndexToArrayItemConverter.shared.cs | 10 +- .../Converters/IntToBoolConverter.shared.cs | 4 +- .../InvertedBoolConverter.shared.cs | 6 +- .../IsNotNullOrEmptyConverter.shared.cs | 4 +- .../IsNullOrEmptyConverter.shared.cs | 6 +- .../ItemSelectedEventArgsConverter.shared.cs | 4 +- .../ItemTappedEventArgsConverter.shared.cs | 4 +- .../ListIsNotNullOrEmptyConverter.shared.cs | 4 +- .../ListIsNullOrEmptyConverter.shared.cs | 8 +- .../ListToStringConverter.shared.cs | 10 +- .../Converters/MultiConverter.shared.cs | 4 +- .../MultiConverterParameter.shared.cs | 4 +- .../Converters/NotEqualConverter.shared.cs | 6 +- .../StateToBooleanConverter.shared.cs | 8 +- .../Converters/TextCaseConverter.shared.cs | 55 +- .../Converters/TextCaseType.shared.cs | 3 + .../TimeSpanToDoubleConverter.shared.cs | 4 +- .../VariableMultiValueConverter.shared.cs | 6 +- .../Core/FileMediaSource.shared.cs | 6 +- .../Core/MediaSource.shared.cs | 17 +- .../Core/MediaSourceConverter.shared.cs | 3 +- .../Core/StreamMediaSource.shared.cs | 18 +- .../Core/UriMediaSource.shared.cs | 4 +- .../Effects/EffectIds.shared.cs | 10 + .../IconTintColorEffectRouter.android.cs | 33 +- .../LifeCycle/LifeCycleEffect.shared.cs | 54 ++ .../LifeCycleEffectRouter.android.cs | 53 ++ .../LifeCycle/LifeCycleEffectRouter.ios.cs | 48 ++ .../LifeCycle/LifeCycleEffectRouter.uwp.cs | 55 ++ .../RemoveBorderEffect.android.cs | 13 +- .../RemoveBorder/RemoveBorderEffect.ios.cs | 2 +- .../RemoveBorder/RemoveBorderEffect.shared.cs | 3 + .../RemoveBorder/RemoveBorderEffect.uwp.cs | 32 + .../SafeArea/SafeAreaEffectRouter.ios.cs | 8 +- .../SelectAllTextEffect.android.cs | 4 +- .../SelectAllText/SelectAllTextEffect.ios.cs | 33 +- .../SelectAllTextEffect.shared.cs | 2 +- .../Shadow/PlatformShadowEffect.android.cs | 90 +++ .../Shadow/PlatformShadowEffect.ios.macos.cs | 107 +++ .../Effects/Shadow/ShadowEffect.shared.cs | 117 ++++ .../LongPressCompletedEventArgs.shared.cs | 12 + .../TouchCompletedEventArgs.shared.cs | 4 +- .../Effects/Touch/GestureManager.shared.cs | 135 ++-- .../Touch/PlatformTouchEffect.android.cs | 88 +-- .../Effects/Touch/PlatformTouchEffect.ios.cs | 62 +- .../Touch/PlatformTouchEffect.macos.cs | 44 +- .../Touch/PlatformTouchEffect.tizen.cs | 32 +- .../Effects/Touch/PlatformTouchEffect.uwp.cs | 75 ++- .../Effects/Touch/TouchEffect.shared.cs | 617 ++++++++++-------- .../Effects/VisualFeedback/TouchEvents.ios.cs | 8 +- .../VisualFeedbackEffectRouter.android.cs | 76 ++- .../VisualFeedbackEffectRouter.ios.cs | 59 +- .../ImageResourceExtension.shared.cs | 10 +- .../JavaObjectExtensions.android.cs | 19 + .../NavigableElementExtensions.shared.cs | 42 ++ .../NavigationExtensions.android.cs | 18 + .../Navigation/NavigationExtensions.ios.cs | 18 + ...andard.macos.tvos.watchos.tizen.wpf.gtk.cs | 16 + .../Navigation/NavigationExtensions.shared.cs | 42 ++ .../Navigation/NavigationExtensions.uwp.cs | 42 ++ .../Extensions/TranslateExtension.shared.cs | 10 +- .../VisualElementExtension.shared.cs | 23 +- .../VisualElementExtensions.uwp.cs | 51 ++ .../DelegateWeakEventManager.shared.cs | 16 +- .../LocalizationResourceManager.shared.cs | 53 +- .../Helpers/LocalizedString.shared.cs | 33 + .../Helpers/SafeArea.shared.cs | 8 +- .../SafeFireAndForgetExtensions.shared.cs | 74 +++ .../Helpers/Subscription.shared.cs | 4 +- .../Helpers/WeakEventManager.shared.cs | 26 +- .../Helpers/WeakEventManagerService.shared.cs | 27 +- .../ObjectModel/AsyncCommand.shared.cs | 22 +- .../ObjectModel/AsyncValueCommand.shared.cs | 22 +- .../CommandFactory.Command.shared.cs | 59 ++ .../CommandFactory.IAsyncCommand.shared.cs | 161 +++++ ...ommandFactory.IAsyncValueCommand.shared.cs | 91 +++ .../INotifyPropertyChangedExtension.shared.cs | 32 + ...servableRangeCollectionExtension.shared.cs | 20 + .../ObjectModel/Grouping.shared.cs | 69 ++ .../Internals/BaseAsyncCommand.shared.cs | 28 +- .../Internals/BaseAsyncValueCommand.shared.cs | 28 +- .../Internals/BaseCommand.android.cs | 10 +- .../ObjectModel/Internals/BaseCommand.gtk.cs | 4 +- .../Internals/BaseCommand.netstandard.wpf.cs | 6 +- .../Internals/BaseCommand.shared.cs | 20 +- .../ObjectModel/ObservableObject.shared.cs | 6 +- .../ObservableRangeCollection.shared.cs | 2 +- .../WindowsSpecific/PopUp.shared.cs | 25 + .../iOSSpecific/PopUp.shared.cs | 36 + .../Startup/ToolkitPlatform.android.cs | 25 + .../Views/AvatarView/AvatarView.shared.cs | 57 +- .../Views/AvatarView/ColorTheme.shared.cs | 1 - .../IImageSourceValidator.shared.cs | 2 +- ...tor.android.ios.macos.uwp.wpf.gtk.tizen.cs | 14 +- .../AvatarView/ImageSourceValidator.shared.cs | 2 +- .../Views/BadgeView/BadgeView.shared.cs | 93 +-- .../Views/BaseTemplatedView.shared.cs | 15 +- .../Android/AutoFitTextureView.android.cs | 6 +- .../Android/CameraCaptureListener.android.cs | 2 +- .../CameraCaptureStateListener.android.cs | 4 +- .../Android/CameraFragment.android.cs | 324 +++++---- .../Android/CameraStateListener.android.cs | 13 +- .../Android/CameraViewRenderer.android.cs | 67 +- .../Android/ImageAvailableListener.android.cs | 16 +- .../Android/MotionEventHelper.android.cs | 9 +- .../Views/CameraView/CameraView.shared.cs | 18 +- .../MediaCapturedEventArgs.shared.cs | 30 +- .../CameraView/UWP/CameraViewRenderer.uwp.cs | 92 +-- .../CameraView/iOS/CameraViewRenderer.ios.cs | 23 +- .../CameraView/iOS/FormsCameraView.ios.cs | 79 ++- .../Views/Expander/Expander.shared.cs | 43 +- .../GravatarImageSourceHandler.android.cs | 9 +- .../GravatarImageSource.shared.cs | 4 +- .../MacOS/GravatarImageSourceHandler.macos.cs | 4 +- ...Handler.android.ios.macos.tizen.uwp.wpf.cs | 8 +- .../GravatarImageSourceHandler.ios.macos.cs | 9 +- .../UWP/GravatarImageSourceHandler.uwp.cs | 4 +- .../Wpf/GravatarImageSourceHandler.wpf.cs | 4 +- .../iOS/GravatarImageSourceHandler.ios.cs | 4 +- .../Views/LazyView/BaseLazyView.shared.cs | 50 ++ .../Views/LazyView/LazyView.shared.cs | 26 + .../Android/FormsVideoView.android.cs | 39 +- .../Android/MediaElementRenderer.android.cs | 115 ++-- .../Views/MediaElement/MediaElement.shared.cs | 24 +- .../UWP/MediaElementRenderer.uwp.cs | 20 +- .../MediaElement/UriTypeConverter.shared.cs | 4 +- .../iOS/MediaElementRenderer.ios.cs | 53 +- .../Popup/Android/PopupRenderer.android.cs | 344 ++++++++++ .../Views/Popup/BasePopup.shared.cs | 180 +++++ .../Views/Popup/Popup.shared.cs | 10 + .../Popup/PopupDismissedEventArgs.shared.cs | 35 + .../Views/Popup/PopupOfT.shared.cs | 55 ++ .../Popup/PopupOpenedEventArgs.shared.cs | 11 + .../Views/Popup/UWP/PopupRenderer.uwp.cs | 291 +++++++++ .../Views/Popup/iOS/PopupRenderer.ios.cs | 287 ++++++++ .../Views/RangeSlider/RangeSlider.shared.cs | 37 +- .../Views/RangeSlider/ThumbFrame.shared.cs | 5 +- .../Views/Shield.shared.cs | 176 +++-- .../Views/SideMenuView/SideMenuView.shared.cs | 73 ++- .../SideMenuViewRenderer.android.cs | 2 +- .../SideMenuView/SideMenuViewRenderer.ios.cs | 8 +- .../Helpers/NativeSnackButton.ios.macos.cs | 63 +- .../Snackbar/Helpers/PaddedLabel.ios.macos.cs | 45 ++ .../Helpers/SnackBarLayout.ios.macos.cs | 10 +- .../Helpers/SnackBarLayout.uwp.wpf.cs | 29 +- .../Snackbar/Helpers/iOS/IOSSnackBar.ios.cs | 19 +- .../Helpers/iOS/SnackBarAppearance.ios.cs | 10 +- .../ActionMessageSnackBarView.ios.cs | 32 +- .../iOS/SnackbarViews/BaseSnackBarView.ios.cs | 26 +- .../SnackbarViews/MessageSnackBarView.ios.cs | 8 +- .../Helpers/macOS/MacOSSnackBar.macos.cs | 13 +- .../Helpers/macOS/SnackBarAppearance.macos.cs | 15 +- .../ActionMessageSnackBarView.macos.cs | 33 +- .../SnackbarViews/BaseSnackBarView.macos.cs | 29 +- .../MessageSnackBarView.macos.cs | 11 +- .../Snackbar/Options/MessageOptions.shared.cs | 9 +- .../Options/SnackBarActionOptions.shared.cs | 13 +- .../Views/Snackbar/SnackBar.android.cs | 44 +- .../Views/Snackbar/SnackBar.gtk.cs | 34 +- .../Views/Snackbar/SnackBar.ios.macos.cs | 35 +- ...s => SnackBar.netstandard.tvos.watchos.cs} | 6 +- .../Views/Snackbar/SnackBar.tizen.cs | 11 +- .../Views/Snackbar/SnackBar.uwp.cs | 6 +- .../Views/Snackbar/SnackBar.wpf.cs | 2 +- .../Views/StateLayout/StateLayout.shared.cs | 22 +- .../StateLayoutController.shared.cs | 20 +- .../Views/StateLayout/StateView.shared.cs | 8 +- .../Views/TabView/TabBadgeView.shared.cs | 86 ++- .../Views/TabView/TabView.shared.cs | 145 ++-- .../Views/TabView/TabViewItem.shared.cs | 34 +- .../Views/ViewToRendererConverter.uwp.cs | 100 +++ .../Xamarin.CommunityToolkit.csproj | 38 +- src/Directory.Build.props | 26 + .../BaseTestFixture.cs | 12 +- .../BindableLayoutExtensionsTests.cs | 14 +- .../BindableObjectExtensionsTests.cs | 134 ++-- .../BindableObjectMultiBindExtensionsTests.cs | 151 +++-- .../BindingHelpers.cs | 96 +-- .../DefaultBindablePropertiesTests.cs | 4 +- .../ElementExtensionsTests.cs | 39 +- .../ElementGesturesExtensionsTests.cs | 36 +- .../FuncConverter.cs | 51 +- .../LabelExtensionsLeftToRightTests.cs | 4 +- .../LabelExtensionsRightToLeftTests.cs | 4 +- .../LabelExtensionsTests.cs | 25 +- .../MarkupBaseTestFixture.cs | 32 +- .../MockPlatformServices.cs | 2 +- .../PaddingElementExtensionsTests.cs | 8 +- .../RelativeLayoutExtensionsTests.cs | 5 +- .../StyleTests.cs | 125 ++-- .../UnitExpressionSearch.cs | 8 +- .../ViewExtensionsLeftToRightTests.cs | 8 +- .../ViewExtensionsRightToLeftTests.cs | 8 +- .../ViewExtensionsTests.cs | 48 +- .../ViewInFlexLayoutExtensionsTests.cs | 10 +- .../ViewInGridExtensionsTests.cs | 20 +- .../VisualElementExtensionsTests.cs | 5 +- ...n.CommunityToolkit.Markup.UnitTests.csproj | 9 +- .../BindableObjectExtensions.cs | 84 +-- .../BindableObjectMultiBindExtensions.cs | 74 +-- .../ElementExtensions.cs | 4 +- .../ElementGesturesExtensions.cs | 18 +- .../FuncConverter.cs | 28 +- .../FuncMultiConverter.cs | 94 +-- .../RelativeLayout.cs | 10 +- .../Xamarin.CommunityToolkit.Markup/Style.cs | 2 +- .../Xamarin.CommunityToolkit.Markup.csproj | 19 +- 454 files changed, 12145 insertions(+), 4647 deletions(-) create mode 100644 samples/XCT.Sample/Images/logo.png create mode 100644 samples/XCT.Sample/Pages/Behaviors/ProgressBarAnimationBehaviorPage.xaml create mode 100644 samples/XCT.Sample/Pages/Behaviors/ProgressBarAnimationBehaviorPage.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Behaviors/SetFocusOnEntryCompletedBehaviorPage.xaml create mode 100644 samples/XCT.Sample/Pages/Behaviors/SetFocusOnEntryCompletedBehaviorPage.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Converters/EnumToBoolConverterPage.xaml create mode 100644 samples/XCT.Sample/Pages/Converters/EnumToBoolConverterPage.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Converters/ImageResourceConverterPage.xaml create mode 100644 samples/XCT.Sample/Pages/Converters/ImageResourceConverterPage.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Effects/LifeCycleEffectPage.xaml create mode 100644 samples/XCT.Sample/Pages/Effects/LifeCycleEffectPage.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Effects/ShadowEffectPage.xaml create mode 100644 samples/XCT.Sample/Pages/Effects/ShadowEffectPage.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Extensions/ImageResourceExtensionPage.xaml create mode 100644 samples/XCT.Sample/Pages/Extensions/ImageResourceExtensionPage.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Views/PopupGalleryPage.xaml create mode 100644 samples/XCT.Sample/Pages/Views/PopupGalleryPage.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Views/Popups/ButtonPopup.xaml create mode 100644 samples/XCT.Sample/Pages/Views/Popups/ButtonPopup.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Views/Popups/CsharpBindingPopup.xaml create mode 100644 samples/XCT.Sample/Pages/Views/Popups/CsharpBindingPopup.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Views/Popups/MultipleButtonPopup.xaml create mode 100644 samples/XCT.Sample/Pages/Views/Popups/MultipleButtonPopup.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Views/Popups/NoLightDismissPopup.xaml create mode 100644 samples/XCT.Sample/Pages/Views/Popups/NoLightDismissPopup.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Views/Popups/OpenedEventSimplePopup.xaml create mode 100644 samples/XCT.Sample/Pages/Views/Popups/OpenedEventSimplePopup.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Views/Popups/PopupAnchorPage.xaml create mode 100644 samples/XCT.Sample/Pages/Views/Popups/PopupAnchorPage.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Views/Popups/PopupPositionPage.xaml create mode 100644 samples/XCT.Sample/Pages/Views/Popups/PopupPositionPage.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Views/Popups/PopupSize.cs create mode 100644 samples/XCT.Sample/Pages/Views/Popups/ReturnResultPopup.xaml create mode 100644 samples/XCT.Sample/Pages/Views/Popups/ReturnResultPopup.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Views/Popups/SimplePopup.xaml create mode 100644 samples/XCT.Sample/Pages/Views/Popups/SimplePopup.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Views/Popups/ToggleSizePopup.xaml create mode 100644 samples/XCT.Sample/Pages/Views/Popups/ToggleSizePopup.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Views/Popups/TransparentPopup.xaml create mode 100644 samples/XCT.Sample/Pages/Views/Popups/TransparentPopup.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Views/Popups/XamlBindingPopup.xaml create mode 100644 samples/XCT.Sample/Pages/Views/Popups/XamlBindingPopup.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Views/TabView/LazyTabPage.xaml create mode 100644 samples/XCT.Sample/Pages/Views/TabView/LazyTabPage.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Views/TabView/LazyTestView.xaml create mode 100644 samples/XCT.Sample/Pages/Views/TabView/LazyTestView.xaml.cs create mode 100644 samples/XCT.Sample/Pages/Views/TabView/NormalTestView.xaml create mode 100644 samples/XCT.Sample/Pages/Views/TabView/NormalTestView.xaml.cs create mode 100644 samples/XCT.Sample/Resx/AppResources.en.resx delete mode 100644 samples/XCT.Sample/Resx/AppResources.es.Designer.cs create mode 100644 samples/XCT.Sample/ViewModels/Behaviors/ProgressBarAnimationBehaviorViewModel.cs create mode 100644 samples/XCT.Sample/ViewModels/Converters/EnumToBoolConverterViewModel.cs create mode 100644 samples/XCT.Sample/ViewModels/Converters/ImageResourceConverterViewModel.cs create mode 100644 samples/XCT.Sample/ViewModels/Extensions/ExtensionsGalleryViewModel.cs create mode 100644 samples/XCT.Sample/ViewModels/Views/PopupControlViewModel.cs create mode 100644 samples/XCT.Sample/ViewModels/Views/Popups/CsharpBindingPopupViewModel.cs create mode 100644 samples/XCT.Sample/ViewModels/Views/Popups/PopupAnchorViewModel.cs create mode 100644 samples/XCT.Sample/ViewModels/Views/Popups/PopupPositionViewModel.cs create mode 100644 samples/XCT.Sample/ViewModels/Views/Popups/XamlBindingPopupViewModel.cs create mode 100644 samples/XCT.Sample/ViewModels/Views/Tabs/LazyTestViewModel.cs create mode 100644 samples/XCT.Sample/ViewModels/Views/Tabs/NormalTestViewModel.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/EventToCommandBehaviorGeneric_Tests.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/SetFocusOnEntryCompleted_Tests.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/EnumToBoolConverter_Tests.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/EnableInitOnlyProperties.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/LocalizedStringTests/LocalizedStringTests.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Mocks/MockEnum.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Mocks/MockItem.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/Grouping_Tests.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/CommandFactoryTests/CommandFactory_AsyncCommand_Tests.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/CommandFactoryTests/CommandFactory_AsyncValueCommand_Tests.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/CommandFactoryTests/CommandFactory_Command_Tests.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/Person.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/AttachedBehaviors/SetFocusOnEntryCompletedBehavior.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/EventToCommandBehaviorGeneric.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/ProgressBarAnimationBehavior.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/EnumToBoolConverter.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ImageResourceConverter.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/LifeCycle/LifeCycleEffect.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/LifeCycle/LifeCycleEffectRouter.android.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/LifeCycle/LifeCycleEffectRouter.ios.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/LifeCycle/LifeCycleEffectRouter.uwp.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/RemoveBorder/RemoveBorderEffect.uwp.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/PlatformShadowEffect.android.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/PlatformShadowEffect.ios.macos.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/ShadowEffect.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/EventArgs/LongPressCompletedEventArgs.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/JavaObjectExtensions.android.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/NavigableElement/NavigableElementExtensions.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.android.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.ios.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.netstandard.macos.tvos.watchos.tizen.wpf.gtk.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.uwp.cs rename src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/{ => VisualElement}/VisualElementExtension.shared.cs (81%) create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/VisualElement/VisualElementExtensions.uwp.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/LocalizedString.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/SafeFireAndForgetExtensions.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/CommandFactory.Command.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/CommandFactory.IAsyncCommand.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/CommandFactory.IAsyncValueCommand.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Extensions/INotifyPropertyChangedExtension.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Extensions/ObservableRangeCollectionExtension.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Grouping.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/PlatformConfiguration/WindowsSpecific/PopUp.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/PlatformConfiguration/iOSSpecific/PopUp.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Startup/ToolkitPlatform.android.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Views/LazyView/BaseLazyView.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Views/LazyView/LazyView.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/Android/PopupRenderer.android.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/BasePopup.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/Popup.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/PopupDismissedEventArgs.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/PopupOfT.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/PopupOpenedEventArgs.shared.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/UWP/PopupRenderer.uwp.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/iOS/PopupRenderer.ios.cs create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/PaddedLabel.ios.macos.cs rename src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/{SnackBar.shared.cs => SnackBar.netstandard.tvos.watchos.cs} (78%) create mode 100644 src/CommunityToolkit/Xamarin.CommunityToolkit/Views/ViewToRendererConverter.uwp.cs create mode 100644 src/Directory.Build.props diff --git a/.editorconfig b/.editorconfig index c71598a71..a596b627e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -107,3 +107,9 @@ csharp_new_line_before_catch = true csharp_new_line_before_finally = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true + +# SA1011: Closing square brackets should be spaced correctly +dotnet_diagnostic.SA1011.severity = none + +# CS4014: Because this call is not awaited, execution of the current method continues before the call is completed +dotnet_diagnostic.CS4014.severity = error diff --git a/Directory.Build.props b/Directory.Build.props index cc26f3910..e7f7d987f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,9 +2,12 @@ - 8.0 + 9.0 + enable + nullable + $(MSBuildThisFileDirectory)Xamarin.CommunityToolkit.ruleset false diff --git a/Xamarin.CommunityToolkit.ruleset b/Xamarin.CommunityToolkit.ruleset index 0d4b31f43..047684820 100644 --- a/Xamarin.CommunityToolkit.ruleset +++ b/Xamarin.CommunityToolkit.ruleset @@ -15,7 +15,6 @@ - diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 956ed9cd8..2e4f772ed 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -6,11 +6,16 @@ variables: NugetPackageVersion: '$(CurrentSemanticVersion)' #MONO_VERSION: 6_4_0 #XCODE_VERSION: 11.4 - NETCORE_VERSION: '3.1.x' - NETCORE_TEST_VERSION: '3.1.x' + NETCORE_VERSION: '5.0.x' + NETCORE_TEST_VERSION_3_1: '3.1.x' + NETCORE_TEST_VERSION_2_1: '2.1.x' RunPoliCheck: 'false' - PathToCsproj: 'src/CommunityToolkit/Xamarin.CommunityToolkit/Xamarin.CommunityToolkit.csproj' PathToMarkupCsproj: 'src/Markup/Xamarin.CommunityToolkit.Markup/Xamarin.CommunityToolkit.Markup.csproj' + PathToCommunityToolkitCsproj: 'src/CommunityToolkit/Xamarin.CommunityToolkit/Xamarin.CommunityToolkit.csproj' + PathToSamplesSln: 'samples/XCT.Sample.sln' + PathToCommunityToolkitUnitTestCsproj: 'src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Xamarin.CommunityToolkit.UnitTests.csproj' + PathToMarkupUnitTestCsproj: 'src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/Xamarin.CommunityToolkit.Markup.UnitTests.csproj' + PathToMsBuildOnMacOS: 'mono /Applications/Visual\ studio.app/Contents/Resources/lib/monodevelop/bin/MSBuild/Current/bin/MSBuild.dll' PathToSln: 'samples/XCT.Sample.sln' resources: @@ -68,6 +73,21 @@ jobs: pool: vmImage: windows-2019 steps: + - task: UseDotNet@2 + displayName: 'Install .NET SDK' + inputs: + version: $(NETCORE_VERSION) + includePreviewVersions: false + - task: UseDotNet@2 + displayName: 'Install .NET 3.1 Test SDK' + inputs: + version: $(NETCORE_TEST_VERSION_3_1) + includePreviewVersions: false + - task: UseDotNet@2 + displayName: 'Install .NET 2.1 Test SDK' + inputs: + version: $(NETCORE_TEST_VERSION_2_1) + includePreviewVersions: false # if this is a tagged build, then update the version number - powershell: | $buildSourceBranch = "$(Build.SourceBranch)" @@ -78,9 +98,9 @@ jobs: condition: startsWith(variables['Build.SourceBranch'], 'refs/tags/') # restore, build and pack the packages - task: MSBuild@1 - displayName: Build Solution + displayName: Build Xamarin.CommunityToolkit.csproj inputs: - solution: $(PathToCsproj) + solution: $(PathToCommunityToolkitCsproj) configuration: Release msbuildArguments: '/restore /t:Build /p:ContinuousIntegrationBuild=true /p:Deterministic=false' - task: CopyFiles@2 @@ -88,9 +108,9 @@ jobs: Contents: 'SignList.xml' TargetFolder: '$(Build.ArtifactStagingDirectory)/nuget' - task: MSBuild@1 - displayName: Pack NuGets + displayName: Pack Community Toolkit NuGets inputs: - solution: $(PathToCsproj) + solution: $(PathToCommunityToolkitCsproj) configuration: Release msbuildArguments: '/t:Pack /p:PackageVersion=$(NugetPackageVersion) /p:PackageOutputPath="$(Build.ArtifactStagingDirectory)/nuget"' - task: MSBuild@1 @@ -112,12 +132,14 @@ jobs: # command: 'custom' # custom: 'nuget' # arguments: 'push --source https://nuget.pkg.github.com/xamarin/index.json --api-key $(GitHub.NuGet.Token) "$(Build.ArtifactStagingDirectory)\nuget\*.nupkg"' - - task: DotNetCoreCLI@2 - displayName: Run Tests + - task: CmdLine@2 + displayName: 'Run Markup Unit Tests' + inputs: + script: dotnet test $(PathToMarkupUnitTestCsproj) -c Release --collect "Code coverage" -p:BuildInParallel=false + - task: CmdLine@2 + displayName: 'Run Community Toolkit Unit Tests' inputs: - command: test - projects: '**/*.UnitTests.csproj' - arguments: '--configuration Release --collect "Code coverage"' + script: dotnet test $(PathToCommunityToolkitUnitTestCsproj) -c Release --collect "Code coverage" -p:BuildInParallel=false # publish the packages - task: PublishBuildArtifacts@1 displayName: 'Publish Unsigned NuGets' @@ -161,27 +183,44 @@ jobs: # displayName: Switch to the latest Xcode # restore, build and pack the packages - task: UseDotNet@2 - displayName: 'Use .Net Core sdk' + displayName: 'Install .NET SDK' inputs: version: $(NETCORE_VERSION) includePreviewVersions: false - task: UseDotNet@2 - displayName: 'Use .Net Core sdk' + displayName: 'Install .NET 3.1 Test SDK' inputs: - version: $(NETCORE_TEST_VERSION) + version: $(NETCORE_TEST_VERSION_3_1) includePreviewVersions: false - - task: MSBuild@1 - displayName: Build Solution + - task: UseDotNet@2 + displayName: 'Install .NET 2.1 Test SDK' inputs: - solution: $(PathToCsproj) - configuration: Release - msbuildArguments: '/restore /t:Build /p:ContinuousIntegrationBuild=true /p:Deterministic=false' - - task: MSBuild@1 - displayName: Pack NuGets + version: $(NETCORE_TEST_VERSION_2_1) + includePreviewVersions: false + - task: CmdLine@2 + displayName: 'Build Markup' inputs: - solution: $(PathToCsproj) - configuration: Release - msbuildArguments: '/t:Pack /p:PackageVersion=$(NugetPackageVersion) /p:PackageOutputPath="$(Build.ArtifactStagingDirectory)/nuget"' + script: '$(PathToMsBuildOnMacOS) $(PathToMarkupCsproj) /p:Configuration=Release /restore /t:Build /p:ContinuousIntegrationBuild=true /p:Deterministic=false' + - task: CmdLine@2 + displayName: 'Build Community Toolkit' + inputs: + script: '$(PathToMsBuildOnMacOS) $(PathToCommunityToolkitCsproj) /p:Configuration=Release /restore /t:Build /p:ContinuousIntegrationBuild=true /p:Deterministic=false' + - task: CmdLine@2 + displayName: 'Run Markup Unit Tests' + inputs: + script: 'dotnet test $(PathToMarkupUnitTestCsproj) /p:Configuration=Release -p:BuildInParallel=false' + - task: CmdLine@2 + displayName: 'Run Community Toolkit Unit Tests' + inputs: + script: 'dotnet test $(PathToCommunityToolkitUnitTestCsproj) /p:Configuration=Release -p:BuildInParallel=false' + - task: CmdLine@2 + displayName: 'Pack Markup NuGets' + inputs: + script: '$(PathToMsBuildOnMacOS) $(PathToMarkupUnitTestCsproj) /p:Configuration=Release /t:Pack /p:PackageVersion=$(NugetPackageVersion) /p:PackageOutputPath="$(Build.ArtifactStagingDirectory)/nuget"' + - task: CmdLine@2 + displayName: 'Pack CommunityToolkit NuGets' + inputs: + script: '$(PathToMsBuildOnMacOS) $(PathToCommunityToolkitCsproj) /p:Configuration=Release /t:Pack /p:PackageVersion=$(NugetPackageVersion) /p:PackageOutputPath="$(Build.ArtifactStagingDirectory)/nuget"' - ${{ if eq(variables['System.TeamProject'], 'devdiv') }}: - template: sign-artifacts/jobs/v2.yml@internal-templates diff --git a/samples/XCT.Sample.Android/MainActivity.cs b/samples/XCT.Sample.Android/MainActivity.cs index 9a00b8c73..d2392c841 100644 --- a/samples/XCT.Sample.Android/MainActivity.cs +++ b/samples/XCT.Sample.Android/MainActivity.cs @@ -2,7 +2,6 @@ using Android.Content.PM; using Android.OS; using Android.Runtime; -using Xamarin.Essentials; namespace Xamarin.CommunityToolkit.Sample.Droid { @@ -17,14 +16,14 @@ protected override void OnCreate(Bundle savedInstanceState) base.OnCreate(savedInstanceState); global::Xamarin.Forms.Forms.SetFlags("CollectionView_Experimental"); - Platform.Init(this, savedInstanceState); + Essentials.Platform.Init(this, savedInstanceState); global::Xamarin.Forms.Forms.Init(this, savedInstanceState); LoadApplication(new App()); } public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Permission[] grantResults) { - Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); + Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); base.OnRequestPermissionsResult(requestCode, permissions, grantResults); } diff --git a/samples/XCT.Sample.Android/SplashActivity.cs b/samples/XCT.Sample.Android/SplashActivity.cs index c9d8f69df..2883e4e55 100644 --- a/samples/XCT.Sample.Android/SplashActivity.cs +++ b/samples/XCT.Sample.Android/SplashActivity.cs @@ -9,7 +9,7 @@ namespace Xamarin.CommunityToolkit.Sample.Droid [Activity(Label = "XamarinCommunityToolkitSample", Icon = "@mipmap/icon", Theme = "@style/SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)] public class SplashActivity : AppCompatActivity { - protected override void OnCreate(Bundle savedInstanceState) + protected override void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); var intent = new Intent(this, typeof(MainActivity)); diff --git a/samples/XCT.Sample.Android/Xamarin.CommunityToolkit.Sample.Android.csproj b/samples/XCT.Sample.Android/Xamarin.CommunityToolkit.Sample.Android.csproj index 8fe84cb44..2a3c8b3ea 100644 --- a/samples/XCT.Sample.Android/Xamarin.CommunityToolkit.Sample.Android.csproj +++ b/samples/XCT.Sample.Android/Xamarin.CommunityToolkit.Sample.Android.csproj @@ -25,7 +25,7 @@ true - portable + full false bin\Debug DEBUG; diff --git a/samples/XCT.Sample.UWP/App.xaml.cs b/samples/XCT.Sample.UWP/App.xaml.cs index 2a60dbd76..29871212c 100644 --- a/samples/XCT.Sample.UWP/App.xaml.cs +++ b/samples/XCT.Sample.UWP/App.xaml.cs @@ -85,7 +85,7 @@ protected override void OnLaunched(LaunchActivatedEventArgs e) /// /// The Frame which failed navigation /// Details about the navigation failure - void OnNavigationFailed(object sender, NavigationFailedEventArgs e) + void OnNavigationFailed(object? sender, NavigationFailedEventArgs e) { throw new Exception("Failed to load Page " + e.SourcePageType.FullName); } @@ -97,7 +97,7 @@ void OnNavigationFailed(object sender, NavigationFailedEventArgs e) /// /// The source of the suspend request. /// Details about the suspend request. - void OnSuspending(object sender, SuspendingEventArgs e) + void OnSuspending(object? sender, SuspendingEventArgs e) { var deferral = e.SuspendingOperation.GetDeferral(); diff --git a/samples/XCT.Sample.iOS/Renderers/NoLineNavigationRenderer.cs b/samples/XCT.Sample.iOS/Renderers/NoLineNavigationRenderer.cs index f1231d834..af6c3fc37 100644 --- a/samples/XCT.Sample.iOS/Renderers/NoLineNavigationRenderer.cs +++ b/samples/XCT.Sample.iOS/Renderers/NoLineNavigationRenderer.cs @@ -25,8 +25,11 @@ public override void ViewWillAppear(bool animated) // Newest iOS version fix - trycatch isn't optimal try { - NavigationBar.ScrollEdgeAppearance.ShadowImage = new UIKit.UIImage(); - NavigationBar.ScrollEdgeAppearance.ShadowColor = null; + if (NavigationBar.ScrollEdgeAppearance != null) + { + NavigationBar.ScrollEdgeAppearance.ShadowImage = new UIKit.UIImage(); + NavigationBar.ScrollEdgeAppearance.ShadowColor = null; + } } catch (Exception) { diff --git a/samples/XCT.Sample.sln b/samples/XCT.Sample.sln index fcf418bc7..4fcef2ef2 100644 --- a/samples/XCT.Sample.sln +++ b/samples/XCT.Sample.sln @@ -18,9 +18,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.CommunityToolkit.Sa EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F695F5E8-420F-475F-A4CF-F5BB3FA6E818}" ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - Directory.Build.props = Directory.Build.props - Xamarin.CommunityToolkit.ruleset = Xamarin.CommunityToolkit.ruleset + ..\.editorconfig = ..\.editorconfig + ..\Directory.Build.props = ..\Directory.Build.props + ..\Xamarin.CommunityToolkit.ruleset = ..\Xamarin.CommunityToolkit.ruleset EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.CommunityToolkit.Sample.WPF", "XCT.Sample.WPF\Xamarin.CommunityToolkit.Sample.WPF.csproj", "{C4D6CD2D-8DF4-4D46-936C-1AB31C87B5EA}" @@ -31,6 +31,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.CommunityToolkit.Sa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Xamarin.CommunityToolkit.Markup", "..\src\Markup\Xamarin.CommunityToolkit.Markup\Xamarin.CommunityToolkit.Markup.csproj", "{A5AAB927-15D7-498C-8295-4209F21836CE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.CommunityToolkit.Markup.UnitTests", "..\src\Markup\Xamarin.CommunityToolkit.Markup.UnitTests\Xamarin.CommunityToolkit.Markup.UnitTests.csproj", "{AAE423C4-E9B4-434E-885C-2164C12BF79C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -62,6 +64,7 @@ Global {8B80BFA6-7B19-4DE7-BD97-7D84194AD0C2}.Debug|x86.Build.0 = Debug|Any CPU {8B80BFA6-7B19-4DE7-BD97-7D84194AD0C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {8B80BFA6-7B19-4DE7-BD97-7D84194AD0C2}.Release|Any CPU.Build.0 = Release|Any CPU + {8B80BFA6-7B19-4DE7-BD97-7D84194AD0C2}.Release|Any CPU.Deploy.0 = Release|Any CPU {8B80BFA6-7B19-4DE7-BD97-7D84194AD0C2}.Release|ARM.ActiveCfg = Release|Any CPU {8B80BFA6-7B19-4DE7-BD97-7D84194AD0C2}.Release|ARM.Build.0 = Release|Any CPU {8B80BFA6-7B19-4DE7-BD97-7D84194AD0C2}.Release|iPhone.ActiveCfg = Release|Any CPU @@ -191,6 +194,8 @@ Global {91C748B4-E9ED-4543-880A-26747B03DE3A}.Debug|x86.Build.0 = Debug|x86 {91C748B4-E9ED-4543-880A-26747B03DE3A}.Debug|x86.Deploy.0 = Debug|x86 {91C748B4-E9ED-4543-880A-26747B03DE3A}.Release|Any CPU.ActiveCfg = Release|x86 + {91C748B4-E9ED-4543-880A-26747B03DE3A}.Release|Any CPU.Build.0 = Release|x86 + {91C748B4-E9ED-4543-880A-26747B03DE3A}.Release|Any CPU.Deploy.0 = Release|x86 {91C748B4-E9ED-4543-880A-26747B03DE3A}.Release|ARM.ActiveCfg = Release|ARM {91C748B4-E9ED-4543-880A-26747B03DE3A}.Release|ARM.Build.0 = Release|ARM {91C748B4-E9ED-4543-880A-26747B03DE3A}.Release|ARM.Deploy.0 = Release|ARM @@ -298,12 +303,37 @@ Global {A5AAB927-15D7-498C-8295-4209F21836CE}.Release|x64.Build.0 = Release|Any CPU {A5AAB927-15D7-498C-8295-4209F21836CE}.Release|x86.ActiveCfg = Release|Any CPU {A5AAB927-15D7-498C-8295-4209F21836CE}.Release|x86.Build.0 = Release|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Debug|ARM.ActiveCfg = Debug|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Debug|ARM.Build.0 = Debug|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Debug|iPhone.Build.0 = Debug|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Debug|x64.ActiveCfg = Debug|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Debug|x64.Build.0 = Debug|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Debug|x86.ActiveCfg = Debug|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Debug|x86.Build.0 = Debug|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Release|Any CPU.Build.0 = Release|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Release|ARM.ActiveCfg = Release|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Release|ARM.Build.0 = Release|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Release|iPhone.ActiveCfg = Release|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Release|iPhone.Build.0 = Release|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Release|x64.ActiveCfg = Release|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Release|x64.Build.0 = Release|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Release|x86.ActiveCfg = Release|Any CPU + {AAE423C4-E9B4-434E-885C-2164C12BF79C}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {54B7812B-45A5-4FE2-9CEA-C5F17D65BDE9} = {47DFE508-04F1-433D-8C55-0C1ACD033573} + {AAE423C4-E9B4-434E-885C-2164C12BF79C} = {47DFE508-04F1-433D-8C55-0C1ACD033573} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {449686DB-B85A-4DFA-AA26-9BC92468CC2A} diff --git a/samples/XCT.Sample/App.xaml.cs b/samples/XCT.Sample/App.xaml.cs index 5ec66b27a..6e2137b3b 100644 --- a/samples/XCT.Sample/App.xaml.cs +++ b/samples/XCT.Sample/App.xaml.cs @@ -1,4 +1,5 @@ -using Xamarin.CommunityToolkit.Helpers; +using System.Globalization; +using Xamarin.CommunityToolkit.Helpers; using Xamarin.CommunityToolkit.Sample.Pages; using Xamarin.CommunityToolkit.Sample.Resx; using Xamarin.Forms.PlatformConfiguration; @@ -11,7 +12,10 @@ public partial class App : Forms.Application public App() { On().SetImageDirectory("Assets"); + + LocalizationResourceManager.Current.PropertyChanged += (sender, e) => AppResources.Culture = LocalizationResourceManager.Current.CurrentCulture; LocalizationResourceManager.Current.Init(AppResources.ResourceManager); + LocalizationResourceManager.Current.CurrentCulture = new CultureInfo("en"); InitializeComponent(); MainPage = new BaseNavigationPage(new WelcomePage()); diff --git a/samples/XCT.Sample/Helpers/RelayCommand.cs b/samples/XCT.Sample/Helpers/RelayCommand.cs index 453460545..d82836760 100644 --- a/samples/XCT.Sample/Helpers/RelayCommand.cs +++ b/samples/XCT.Sample/Helpers/RelayCommand.cs @@ -7,25 +7,21 @@ namespace Xamarin.CommunityToolkit.Sample { public class RelayCommand : ICommand { - readonly Action execute; - readonly Func asyncExecute; + readonly Action? execute; + readonly Func? asyncExecute; + readonly Func? canExecute; - Func canExecute; int executingCount; - public RelayCommand(Action execute, Func canExecute = null) + public RelayCommand(Action execute, Func? canExecute = null) { - if (execute == null) - throw new ArgumentNullException(nameof(execute)); - this.execute = execute; + this.execute = execute ?? throw new ArgumentNullException(nameof(execute)); this.canExecute = canExecute; } - protected RelayCommand(Func execute, Func canExecute = null) // This ctor is protected here and public in a derived class, to allow simple initialization like new RelayCommand(MyMethod) without errors due to ambiguity + protected RelayCommand(Func execute, Func? canExecute = null) // This ctor is protected here and public in a derived class, to allow simple initialization like new RelayCommand(MyMethod) without errors due to ambiguity { - if (execute == null) - throw new ArgumentNullException(nameof(execute)); - asyncExecute = execute; + asyncExecute = execute ?? throw new ArgumentNullException(nameof(execute)); this.canExecute = canExecute; } @@ -34,7 +30,7 @@ public RelayCommand(Action execute, Func canExecute = null) /// /// Ignored; this is the paremeterless command class /// - public bool CanExecute(object parameter = null) + public bool CanExecute(object? parameter = null) { try { @@ -47,15 +43,12 @@ public bool CanExecute(object parameter = null) } } - public event EventHandler CanExecuteChanged; + public event EventHandler? CanExecuteChanged; - public void RaiseCanExecuteChanged() - { - CanExecuteChanged?.Invoke(this, EventArgs.Empty); - } + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); // Asynchronous command handling based on http://stackoverflow.com/a/31595509/6043538 - public async void Execute(object parameter = null) + public async void Execute(object? parameter = null) { var couldExecuteBeforeExecute = CanExecute(); if (!couldExecuteBeforeExecute) @@ -70,8 +63,10 @@ public async void Execute(object parameter = null) { if (execute != null) execute(); - else + else if (asyncExecute != null) await asyncExecute(); + else + throw new Exception("Execute is null"); } catch (Exception ex) { @@ -89,31 +84,30 @@ public async void Execute(object parameter = null) public class RelayCommandAsync : RelayCommand { - public RelayCommandAsync(Func execute, Func canExecute = null) - : base(execute, canExecute) { } // This ctor is public here and protected in the base class, to allow simple initialization like new RelayCommandAsync(MyMethod) without errors due to ambiguity + public RelayCommandAsync(Func execute, Func? canExecute = null) + : base(execute, canExecute) + { + // This ctor is public here and protected in the base class, to allow simple initialization like new RelayCommandAsync(MyMethod) without errors due to ambiguity + } } public class RelayCommand : ICommand { - readonly Action execute; - readonly Func asyncExecute; + readonly Action? execute; + readonly Func? asyncExecute; + readonly Func? canExecute; - Func canExecute; int executingCount; - public RelayCommand(Action execute, Func canExecute = null) + public RelayCommand(Action execute, Func? canExecute = null) { - if (execute == null) - throw new ArgumentNullException(nameof(execute)); - this.execute = execute; + this.execute = execute ?? throw new ArgumentNullException(nameof(execute)); this.canExecute = canExecute; } - protected RelayCommand(Func execute, Func canExecute = null) // This ctor is protected here and public in a derived class, to allow simple initialization like new RelayCommand(MyMethod) without errors due to ambiguity + protected RelayCommand(Func execute, Func? canExecute = null) // This ctor is protected here and public in a derived class, to allow simple initialization like new RelayCommand(MyMethod) without errors due to ambiguity { - if (execute == null) - throw new ArgumentNullException(nameof(execute)); - asyncExecute = execute; + asyncExecute = execute ?? throw new ArgumentNullException(nameof(execute)); this.canExecute = canExecute; } @@ -122,11 +116,11 @@ public RelayCommand(Action execute, Func canExecut /// /// /// - public bool CanExecute(object parameter = null) + public bool CanExecute(object? parameter = null) { try { - return canExecute != null ? canExecute((TParameter)parameter) : executingCount == 0; + return canExecute != null ? canExecute((TParameter?)parameter) : executingCount == 0; } catch (Exception ex) { @@ -135,12 +129,9 @@ public bool CanExecute(object parameter = null) } } - public event EventHandler CanExecuteChanged; + public event EventHandler? CanExecuteChanged; - public void RaiseCanExecuteChanged() - { - CanExecuteChanged?.Invoke(this, EventArgs.Empty); - } + public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); // Asynchronous command handling based on http://stackoverflow.com/a/31595509/6043538 public async void Execute(object parameterAsObject) @@ -159,13 +150,11 @@ public async void Execute(object parameterAsObject) var parameter = (TParameter)parameterAsObject; if (execute != null) - { execute(parameter); - } - else - { + else if (asyncExecute != null) await asyncExecute(parameter); - } + else + throw new Exception("Execute is null"); } catch (Exception ex) { @@ -183,7 +172,10 @@ public async void Execute(object parameterAsObject) public class RelayCommandAsync : RelayCommand { - public RelayCommandAsync(Func execute, Func canExecute = null) - : base(execute, canExecute) { } // This ctor is public here and protected in the base class, to allow simple initialization like new RelayCommandAsync(MyMethod) without errors due to ambiguity + public RelayCommandAsync(Func execute, Func? canExecute = null) + : base(execute, canExecute) + { + // This ctor is public here and protected in the base class, to allow simple initialization like new RelayCommandAsync(MyMethod) without errors due to ambiguity + } } } \ No newline at end of file diff --git a/samples/XCT.Sample/Helpers/XLog.cs b/samples/XCT.Sample/Helpers/XLog.cs index 88dd1c65c..79e77b027 100644 --- a/samples/XCT.Sample/Helpers/XLog.cs +++ b/samples/XCT.Sample/Helpers/XLog.cs @@ -13,7 +13,7 @@ namespace Xamarin.CommunityToolkit.Sample /// public static class XLog { - static string rootFolderPattern = null; + static string? rootFolderPattern = null; #if WINDOWS_UWP static LoggingChannel loggingChannel; #endif @@ -22,7 +22,7 @@ public static class XLog /// Call this before logging starts. /// /// Should match the top folder name(s) within the source control repository, e.g. @"\MobileRealtimePush\MobileRealtimePush\". Any folders before the first match of this pattern are omitted from the logged source file paths - public static void Init(string rootFolderPattern = null) + public static void Init(string? rootFolderPattern = null) { XLog.rootFolderPattern = rootFolderPattern; #if WINDOWS_UWP @@ -46,10 +46,10 @@ public static void Init(string rootFolderPattern = null) /// supplied by compiler, no need to specify in code unless you want to pass a deeper call context [Conditional("DEBUG")] public static void Debug( - object data = null, - string tag = null, - [CallerMemberName] string memberName = null, - [CallerFilePath] string sourceFilePath = null, + object? data = null, + string? tag = null, + [CallerMemberName] string? memberName = null, + [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = -1) { var message = FormatLogString(data, tag, memberName, sourceFilePath, sourceLineNumber); @@ -75,10 +75,10 @@ public static void Init(string rootFolderPattern = null) /// supplied by compiler, no need to specify in code unless you want to pass a deeper call context [Conditional("TRACE")] public static void Trace( - object data = null, - string tag = null, - [CallerMemberName] string memberName = null, - [CallerFilePath] string sourceFilePath = null, + object? data = null, + string? tag = null, + [CallerMemberName] string? memberName = null, + [CallerFilePath] string? sourceFilePath = null, [CallerLineNumber] int sourceLineNumber = -1) { var message = FormatLogString(data, tag, memberName, sourceFilePath, sourceLineNumber); @@ -90,9 +90,9 @@ public static void Init(string rootFolderPattern = null) #endif } - public static string TruncateAt(this string s, int maxLength, string truncatedSuffix = "...") => s?.Length <= maxLength ? s : s.Substring(0, maxLength) + truncatedSuffix; + public static string TruncateAt(this string? s, int maxLength, string truncatedSuffix = "...") => s?.Length <= maxLength ? s : s?.Substring(0, maxLength) + truncatedSuffix; - static string FormatLogString(object data = null, string tag = null, string memberName = null, string sourceFilePath = null, int sourceLineNumber = -1) + static string FormatLogString(object? data, string? tag, string? memberName, string? sourceFilePath, int sourceLineNumber) { var line = new StringBuilder(); @@ -121,7 +121,7 @@ static string FormatLogString(object data = null, string tag = null, string memb line.Append(dataString); } - if (!string.IsNullOrEmpty(sourceFilePath)) + if (sourceFilePath != null && !string.IsNullOrEmpty(sourceFilePath)) { if (!string.IsNullOrEmpty(rootFolderPattern)) { diff --git a/samples/XCT.Sample/Images/logo.png b/samples/XCT.Sample/Images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..633b9c0b06200d446e8ce4267f8c3d714f15d6df GIT binary patch literal 4630 zcmZu#c{CJW*d7eVlE#d^NrOrDJ%(f%hOuUseI&B)yT}Y`EZO&^WXX~xVv3BcqY|=@ zkz|RirGyf``F-bn|9$7&^}X+X?{lB~p1UQRnd+Zo6<`Gb0Ot%ax)!wj?SB!(L`xfQ zHa((cEP)ui5CDJ;`d_326qoYRgmfVm`Y1rtC~SjP0p8Iz(FOooGT8pPG6Ddc&kS_6 zts?1m%A5kNr@4n_HB?wYEFR$T0q=g}98Z1Uc%)m|2Kdokz(xC5KrC3TGM<=XbLp?{ z&zT}3Yh^z0QX=!4&b}*`u_(qFTI7{tV2ynV_kJgNT0oI(l#FN$skmM zGtc0HV&9pI4?fRLf+h{xrn3jQDAI`wU!~umo_RTxSs!*u6a1tItC?_)wXepDKbm6n zgc$i`!D)P9vjT^d2oDvxQ1Hi?VEvd&eCgx#y|w1Dn;Ax#em$8w@qA@jiWiL2Mais* zgRV?ZQwK1(=g1Fzx3g|y;ZhtgdSa3e*?*zs*jIoP*X3Ft47v(Nj%_~8=etwIcXcqh z!O{GXQO-jcdy6Rdnp{C`pZ&{p);M?p0yXd?$ZW z?T(QI{(0lD*?bLJ0$)g+%o&ddSePfKX?>h&*Zd903B$~F-v;&(WmfP+*}Rnw1h8+& zv&cg1;+ToLK?EeER&!(;ToeUviG^msN8==Um{$ggu_2Qt$(I@omHsn5g>ao$83*Ad z*LvipF_Z%nWBA>){5^xQ-szS@%G&qu3L+sNqAQ+0;h$w7~8 zE)HnX9IGl_Bpr3#%L>2MQGf;OGt>j zG3IBTWd8mm#hT)!&p9!0``9}Jj&`u6&tZ44QR$!D%D*-#@uusTWg~h5?XR>l2M`z+ z(0;XH(u$p;OwDn9laEc{{gN=&&FJ>g_t-nEMNFAAH`Vb$5GiRA5MYqh`!kmI4d$ohG-+ZV<55ULjzETko zIi(+6x%2EY-t1ZWO#|5Op@X3glFs zK>K{9`HXfqjRewQ+7+LfADf`;%@@-2=fujX`%Ef}q^M1c@*ynOTyV3hK}^l{Jodgh zqsdGgE0q;oxII#JBfvBno6cbT_|BrlwtK(=u*OC!`W$6I}X4zNe{G>@iqpm6Afw;qjJ3VjU>y zAhWq9XuE`)7!FDF*UoGYONd|A%IZ>GmBrPFW$y(foYzYec6I8sUq;4cQPhlp^D4Po z6O{e6o_9v%l-ob;dn34{7@ay_ULr}FA@&p-1zyAcMT{FJdZ^C$D)nFT>CNaJmkiweWPK+- zLlyr{XXqz7o$ya^X;4KVo6B#p7ki!sK<&(59>H3%C_H3UxIQ^}En|T{ayXeIutx5Y z;uAyd_d7M=VX*y}*m4-c$Rxn{y}=J91Y;05&li6x&uXbWYVL77FrzBxG(=1b04=(ZLiSSD)sC}YVdH`DmsHk9 zN7$EL4RLjv3*P+x>FgS_b7Fc=&uyo=`i~fa=QP={bw@&7+S| zx8^Sc1;@VJecdCbgGTf=Zi|7wbJ(e5|64s)>96QaGQFNEqHw<9)6oZ^Pdi(>nGN4( zrqQ{W<#2MJMAFw3iSj^>f{i<)%Vr7Bk{ydv<;>v52rnla%~^i7S+QuUK-r7s<Q9 zmxEEXWGiyYb@7Gr2efYfXwemesU1=O6B*f8tQtRlL^MTgxd+t($C_tZTpQP|Lr#Ep zL9!3#(eE8>Uu6dtexoQp`yKuR?9?-Wn%cY)H@qw7ooV*%eY^kNU0XO-Uakd6PngJL~?CwYFdre&hn`js$yQM%UR< zgyk&dfiMpy>;3(?sot>k+OkJ-tfUi48CmJVRSvoX)RW74xMc1KPBX?=jNKErgrS_` z`qFlVC)eZ*d&SM#>7M;P@(q$sL{|u~%sjuW%}!s*9OQ>A<87ip`S=D$6{OIU7<}&g-SO&tbxmAlQ{IvDKfm;@Eh-b*tdF`@oE154LBbtohpvWM1t7`7`+l0hSoU);fV(eYp_cIeG& zST>)wLxiXPI$k-!eI#OjnL5gHd^eQ9z!-@Aww1EDGB|MbcDShZPlO$gD<@ZdzI#^O zA7mw)Zm&w0iV#&#juPiC2)I)ta1hKTo>IBUZ@ib{cG@llal+oxV0*zV8T+O}3pUTo zT4YLb3aX_t;S+;$ZX*6>T30X&moqJ#hz#xAxSlCrOQ__!7?qi_IBfNPuG86jG1kE~ zQ(i1(g&Y!yFi*;RaA~&d=?o)!-WG;S)3S#BdYp=eaJPk>9rz9fHg4U0h__Syh2nvP zf1*d((OPrRkR(H56R(Si5F1hU5p+oT&eWj%S%)3sNo`hP!E%gJ`A(|AxhilTn#*w+ zbJq^h+bi!|bB+|(d?*cWi*;v(hFj7)+-br5gM_lrwndSu-W^l3f+-9KbzY@{b z%)~v3NMw=WF6HYlOv>??_gw1DPsyU^$bFksJMcx{F?Rh&^qS~DYNs=b1qcm?`njmrJ$R67 zg))H2NM-+_Bp-CkjWe+L{Vnl0-b9iO&Hob%Yx_CLK4HY0>tb3ivcr9r1IMKyd8zfC zgia;S4esJVb>09Ep24LGON7I&AJa1XBkFVzSQvaF9sg?Q^__md^5Ozb0TmQXrc<}| zcelrc`GBgLVAk8_(U#a`8~4y_{qyHJmEelVdV}y7ADkO(r2&QBdznUh(Hz-D3?p ze{%mflC?SZ*L<5LxhWHkI!LYaATns_wa-p^Uq&X3$* ztMjsP>+_58g3FO3G_Bvdjp=+zy1?ui5A`&`Y?x#Vwab}u;O@=&pW(S;X&AatVAVa20fDKY zH_)L0yVRLJ&Lp%~?;@(Q;=0dSO`8gYmrzHuMbh<(u7p6sMCK0M4Gn1YqENIuEKNzr ziv@08GRrpKoi_62UD-~unr-z*iIEXvz}ge5BB;=1@OxVK6SL=Gq)eY(t#%q--m+=V zN-f$keCz?uGYPOlNf^kwJDttR0tw1kiyITijrH0@wD^4Y9Pa0+e=8@D_S8E;PIE@Y zWo(xSEr`x)js0|+(vg>!O={DsrGc12jne?x!$I@i8;SLeTS;A7uO!^mvRJrXL981H z5?9JdWMQBgkp>|H9)eXA6us!_Pn(V#ux})CLK{ZIaLW`(TcsQ(W6TF5c|*8k-gqFp z!T%OPm&UF_4Tn4vu%%ib4+iZGq#z+;O;HGkMF}Y~rOUbV;2t#}+Og8LQlQ*~G%m64 z;FAO=qEQzn2XTDT;DX%~5z>L=90NPQZ_xIzZ0E$auGoj;O>PoEJaQZuOhOT^;0%Bb z_dR9l1zRi0&N-EO;&q$^qyPKG*x{WKk=kRhi&qvZ=jZ!c4o_sPM5QcsNtKF8bq5dZ j6ME&?`~UwS^hdMGaRIEP6N06E;{XiMrn*fiEdGB0nQV{{ literal 0 HcmV?d00001 diff --git a/samples/XCT.Sample/Pages/AboutPage.xaml b/samples/XCT.Sample/Pages/AboutPage.xaml index 8fbf440c9..9abebc964 100644 --- a/samples/XCT.Sample/Pages/AboutPage.xaml +++ b/samples/XCT.Sample/Pages/AboutPage.xaml @@ -14,7 +14,11 @@ - + + + + + diff --git a/samples/XCT.Sample/Pages/Effects/TouchEffectPage.xaml.cs b/samples/XCT.Sample/Pages/Effects/TouchEffectPage.xaml.cs index 99065e1d3..67253a5a1 100644 --- a/samples/XCT.Sample/Pages/Effects/TouchEffectPage.xaml.cs +++ b/samples/XCT.Sample/Pages/Effects/TouchEffectPage.xaml.cs @@ -1,4 +1,6 @@ using System.Windows.Input; +using Xamarin.CommunityToolkit.ObjectModel; +using Xamarin.Forms.Xaml; using Xamarin.Forms; namespace Xamarin.CommunityToolkit.Sample.Pages.Effects @@ -7,18 +9,18 @@ public partial class TouchEffectPage { public TouchEffectPage() { - Command = new Command(() => + Command = CommandFactory.Create(() => { TouchCount++; OnPropertyChanged(nameof(TouchCount)); }); - LongPressCommand = new Command(() => + LongPressCommand = CommandFactory.Create(() => { LongPressCount++; OnPropertyChanged(nameof(LongPressCount)); }); - InitializeComponent(); + InitializeComponent(); } public ICommand Command { get; } diff --git a/samples/XCT.Sample/Pages/Extensions/ExtensionsGalleryPage.xaml b/samples/XCT.Sample/Pages/Extensions/ExtensionsGalleryPage.xaml index f542ac2ad..7de800120 100644 --- a/samples/XCT.Sample/Pages/Extensions/ExtensionsGalleryPage.xaml +++ b/samples/XCT.Sample/Pages/Extensions/ExtensionsGalleryPage.xaml @@ -1,9 +1,14 @@ - - + - - - + ControlTemplate="{StaticResource GalleryPageTemplate}"> + + + + + + \ No newline at end of file diff --git a/samples/XCT.Sample/Pages/Extensions/ExtensionsGalleryPage.xaml.cs b/samples/XCT.Sample/Pages/Extensions/ExtensionsGalleryPage.xaml.cs index 49ba868bd..bc952a6d0 100644 --- a/samples/XCT.Sample/Pages/Extensions/ExtensionsGalleryPage.xaml.cs +++ b/samples/XCT.Sample/Pages/Extensions/ExtensionsGalleryPage.xaml.cs @@ -2,7 +2,6 @@ { public partial class ExtensionsGalleryPage : BasePage { - public ExtensionsGalleryPage() - => InitializeComponent(); + public ExtensionsGalleryPage() => InitializeComponent(); } } \ No newline at end of file diff --git a/samples/XCT.Sample/Pages/Extensions/ImageResourceExtensionPage.xaml b/samples/XCT.Sample/Pages/Extensions/ImageResourceExtensionPage.xaml new file mode 100644 index 000000000..3a2821bb6 --- /dev/null +++ b/samples/XCT.Sample/Pages/Extensions/ImageResourceExtensionPage.xaml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/samples/XCT.Sample/Pages/Extensions/ImageResourceExtensionPage.xaml.cs b/samples/XCT.Sample/Pages/Extensions/ImageResourceExtensionPage.xaml.cs new file mode 100644 index 000000000..fe26c18b4 --- /dev/null +++ b/samples/XCT.Sample/Pages/Extensions/ImageResourceExtensionPage.xaml.cs @@ -0,0 +1,7 @@ +namespace Xamarin.CommunityToolkit.Sample.Pages.Effects +{ + public partial class ImageResourceExtensionPage + { + public ImageResourceExtensionPage() => InitializeComponent(); + } +} \ No newline at end of file diff --git a/samples/XCT.Sample/Pages/Markup/SearchPage.logic.cs b/samples/XCT.Sample/Pages/Markup/SearchPage.logic.cs index 1960071c3..7016b750f 100644 --- a/samples/XCT.Sample/Pages/Markup/SearchPage.logic.cs +++ b/samples/XCT.Sample/Pages/Markup/SearchPage.logic.cs @@ -1,4 +1,5 @@ -using Xamarin.CommunityToolkit.Sample.Pages; +using System.Threading.Tasks; +using Xamarin.CommunityToolkit.Sample.Pages; using Xamarin.CommunityToolkit.Sample.ViewModels.Markup; using Xamarin.Forms; using Xamarin.Forms.PlatformConfiguration; @@ -9,21 +10,21 @@ namespace Xamarin.CommunityToolkit.Sample public partial class SearchPage : BasePage { readonly SearchViewModel vm; - View header; + View? header; public SearchPage() { On().SetUseSafeArea(true); BackgroundColor = Color.Black; - BindingContext = new SearchViewModel(); + BindingContext = vm = new SearchViewModel(); Build(); } - void Search_FocusChanged(object sender, FocusEventArgs e) + async void Search_FocusChanged(object? sender, FocusEventArgs e) { ViewExtensions.CancelAnimations(header); - header.TranslateTo(e.IsFocused ? -56 : 0, 0, 250, Easing.CubicOut); + await (header?.TranslateTo(e.IsFocused ? -56 : 0, 0, 250, Easing.CubicOut) ?? Task.CompletedTask); } } } \ No newline at end of file diff --git a/samples/XCT.Sample/Pages/SettingPage.xaml b/samples/XCT.Sample/Pages/SettingPage.xaml index e0da4d86b..78c8a8ff3 100644 --- a/samples/XCT.Sample/Pages/SettingPage.xaml +++ b/samples/XCT.Sample/Pages/SettingPage.xaml @@ -11,7 +11,7 @@ - + @@ -26,5 +26,7 @@ - + + diff --git a/samples/XCT.Sample/Pages/Views/SnackBarPage.xaml.cs b/samples/XCT.Sample/Pages/Views/SnackBarPage.xaml.cs index 2845fe2fb..24a10912d 100644 --- a/samples/XCT.Sample/Pages/Views/SnackBarPage.xaml.cs +++ b/samples/XCT.Sample/Pages/Views/SnackBarPage.xaml.cs @@ -14,7 +14,7 @@ public partial class SnackBarPage : BasePage { public SnackBarPage() => InitializeComponent(); - async void DisplaySnackBarClicked(object sender, EventArgs args) + async void DisplaySnackBarClicked(object? sender, EventArgs args) { var result = await this.DisplaySnackBarAsync(GenerateLongText(5), "Run action", () => { @@ -24,57 +24,74 @@ async void DisplaySnackBarClicked(object sender, EventArgs args) StatusText.Text = result ? "SnackBar is closed by user" : "SnackBar is closed by timeout"; } - async void DisplayToastClicked(object sender, EventArgs args) + async void DisplaySnackBarWithPadding(object? sender, EventArgs args) + { + var options = new SnackBarOptions() + { + BackgroundColor = Color.FromHex("#CC0000"), + MessageOptions = new MessageOptions + { + Message = "msg", + Foreground = Color.White, + Font = Font.SystemFontOfSize(16), + Padding = new Thickness(10, 20, 30, 40) + } + }; + + await this.DisplaySnackBarAsync(options); + } + + async void DisplayToastClicked(object? sender, EventArgs args) { await this.DisplayToastAsync(GenerateLongText(5)); StatusText.Text = "Toast is closed by timeout"; } - async void DisplaySnackBarAdvancedClicked(object sender, EventArgs args) + async void DisplaySnackBarAdvancedClicked(object? sender, EventArgs args) { const string SmileIcon = "\uf118"; - var messageOptions = new MessageOptions - { - Foreground = Color.DeepSkyBlue, - Font = Font.OfSize("FARegular", 40), - Message = SmileIcon - }; - - var actionOptions = new List + var options = new SnackBarOptions { - new SnackBarActionOptions + MessageOptions = new MessageOptions { - ForegroundColor = Color.Red, - BackgroundColor = Color.Green, - Font = Font.OfSize("Times New Roman", 15), - Text = "Action1", - Action = () => - { - Debug.WriteLine("1"); - return Task.CompletedTask; - } + Foreground = Color.DeepSkyBlue, + Font = Font.OfSize("FARegular", 40), + Padding = new Thickness(10, 20, 30, 40), + Message = SmileIcon }, - new SnackBarActionOptions + Duration = TimeSpan.FromMilliseconds(5000), + BackgroundColor = Color.Coral, + IsRtl = CultureInfo.CurrentCulture.TextInfo.IsRightToLeft, + Actions = new List { - ForegroundColor = Color.Green, - BackgroundColor = Color.Red, - Font = Font.OfSize("Times New Roman", 20), - Text = "Action2", - Action = () => + new SnackBarActionOptions + { + ForegroundColor = Color.Red, + BackgroundColor = Color.Green, + Font = Font.OfSize("Times New Roman", 15), + Padding = new Thickness(10, 20, 30, 40), + Text = "Action1", + Action = () => + { + Debug.WriteLine("1"); + return Task.CompletedTask; + } + }, + new SnackBarActionOptions { - Debug.WriteLine("2"); - return Task.CompletedTask; + ForegroundColor = Color.Green, + BackgroundColor = Color.Red, + Font = Font.OfSize("Times New Roman", 20), + Padding = new Thickness(40, 30, 20, 10), + Text = "Action2", + Action = () => + { + Debug.WriteLine("2"); + return Task.CompletedTask; + } } } }; - var options = new SnackBarOptions - { - MessageOptions = messageOptions, - Duration = TimeSpan.FromMilliseconds(5000), - BackgroundColor = Color.Coral, - IsRtl = CultureInfo.CurrentCulture.TextInfo.IsRightToLeft, - Actions = actionOptions - }; var result = await this.DisplaySnackBarAsync(options); StatusText.Text = result ? "SnackBar is closed by user" : "SnackBar is closed by timeout"; } diff --git a/samples/XCT.Sample/Pages/Views/TabView/CustomTabsPage.xaml.cs b/samples/XCT.Sample/Pages/Views/TabView/CustomTabsPage.xaml.cs index b2649bde9..8e8273717 100644 --- a/samples/XCT.Sample/Pages/Views/TabView/CustomTabsPage.xaml.cs +++ b/samples/XCT.Sample/Pages/Views/TabView/CustomTabsPage.xaml.cs @@ -6,6 +6,6 @@ public partial class CustomTabsPage : BasePage { public CustomTabsPage() => InitializeComponent(); - void OnFabTabTapped(object sender, TabTappedEventArgs e) => DisplayAlert("FabTabGallery", "Tab Tapped.", "Ok"); + void OnFabTabTapped(object? sender, TabTappedEventArgs e) => DisplayAlert("FabTabGallery", "Tab Tapped.", "Ok"); } } \ No newline at end of file diff --git a/samples/XCT.Sample/Pages/Views/TabView/LazyTabPage.xaml b/samples/XCT.Sample/Pages/Views/TabView/LazyTabPage.xaml new file mode 100644 index 000000000..a08c4ca21 --- /dev/null +++ b/samples/XCT.Sample/Pages/Views/TabView/LazyTabPage.xaml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/XCT.Sample/Pages/Views/TabView/LazyTabPage.xaml.cs b/samples/XCT.Sample/Pages/Views/TabView/LazyTabPage.xaml.cs new file mode 100644 index 000000000..fe53d1b9c --- /dev/null +++ b/samples/XCT.Sample/Pages/Views/TabView/LazyTabPage.xaml.cs @@ -0,0 +1,7 @@ +namespace Xamarin.CommunityToolkit.Sample.Pages.Views.TabView +{ + public partial class LazyTabPage + { + public LazyTabPage() => InitializeComponent(); + } +} \ No newline at end of file diff --git a/samples/XCT.Sample/Pages/Views/TabView/LazyTestView.xaml b/samples/XCT.Sample/Pages/Views/TabView/LazyTestView.xaml new file mode 100644 index 000000000..68c8ab19a --- /dev/null +++ b/samples/XCT.Sample/Pages/Views/TabView/LazyTestView.xaml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/samples/XCT.Sample/Pages/Views/TabView/LazyTestView.xaml.cs b/samples/XCT.Sample/Pages/Views/TabView/LazyTestView.xaml.cs new file mode 100644 index 000000000..24fb78a44 --- /dev/null +++ b/samples/XCT.Sample/Pages/Views/TabView/LazyTestView.xaml.cs @@ -0,0 +1,29 @@ +using Xamarin.CommunityToolkit.Sample.ViewModels.Views.Tabs; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.Sample.Pages.Views.TabView +{ + public partial class LazyTestView : ContentView + { + public LazyTestView() + { + InitializeComponent(); + + Build(); + NormalTestViewModel.Current.LoadedViews += "LazyView Loaded \n"; + } + + void Build() + { + for (var i = 0; i < 117; i++) + { + var box = new BoxView + { + BackgroundColor = i % 2 == 0 ? Color.Blue : Color.Fuchsia + }; + + uniformGrid.Children.Add(box); + } + } + } +} \ No newline at end of file diff --git a/samples/XCT.Sample/Pages/Views/TabView/NormalTestView.xaml b/samples/XCT.Sample/Pages/Views/TabView/NormalTestView.xaml new file mode 100644 index 000000000..2026ef55d --- /dev/null +++ b/samples/XCT.Sample/Pages/Views/TabView/NormalTestView.xaml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/samples/XCT.Sample/Pages/Views/TabView/NormalTestView.xaml.cs b/samples/XCT.Sample/Pages/Views/TabView/NormalTestView.xaml.cs new file mode 100644 index 000000000..f792cf33a --- /dev/null +++ b/samples/XCT.Sample/Pages/Views/TabView/NormalTestView.xaml.cs @@ -0,0 +1,16 @@ +using Xamarin.CommunityToolkit.Sample.ViewModels.Views.Tabs; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.Sample.Pages.Views.TabView +{ + public partial class NormalTestView : ContentView + { + public NormalTestView() + { + InitializeComponent(); + BindingContext = NormalTestViewModel.Current; + + NormalTestViewModel.Current.LoadedViews += "NormalTestLoaded \n"; + } + } +} \ No newline at end of file diff --git a/samples/XCT.Sample/Pages/Views/TabView/TabBadgePage.xaml.cs b/samples/XCT.Sample/Pages/Views/TabView/TabBadgePage.xaml.cs index 41dab454f..c5293b6c9 100644 --- a/samples/XCT.Sample/Pages/Views/TabView/TabBadgePage.xaml.cs +++ b/samples/XCT.Sample/Pages/Views/TabView/TabBadgePage.xaml.cs @@ -26,9 +26,9 @@ public int Counter } } - void OnIncreaseClicked(object sender, EventArgs e) => Counter++; + void OnIncreaseClicked(object? sender, EventArgs e) => Counter++; - void OnDecreaseClicked(object sender, EventArgs e) + void OnDecreaseClicked(object? sender, EventArgs e) { if (Counter == 0) return; diff --git a/samples/XCT.Sample/Pages/Views/TabView/TabPlacementPage.xaml.cs b/samples/XCT.Sample/Pages/Views/TabView/TabPlacementPage.xaml.cs index 2a4ec087b..a56f5f52a 100644 --- a/samples/XCT.Sample/Pages/Views/TabView/TabPlacementPage.xaml.cs +++ b/samples/XCT.Sample/Pages/Views/TabView/TabPlacementPage.xaml.cs @@ -7,7 +7,7 @@ public partial class TabPlacementPage : BasePage { public TabPlacementPage() => InitializeComponent(); - void OnChangeTabStripPlacementClicked(object sender, EventArgs e) + void OnChangeTabStripPlacementClicked(object? sender, EventArgs e) { if (TabView.TabStripPlacement == TabStripPlacement.Bottom) TabView.TabStripPlacement = TabStripPlacement.Top; diff --git a/samples/XCT.Sample/Pages/WelcomePage.xaml.cs b/samples/XCT.Sample/Pages/WelcomePage.xaml.cs index 7fe36d455..60392f552 100644 --- a/samples/XCT.Sample/Pages/WelcomePage.xaml.cs +++ b/samples/XCT.Sample/Pages/WelcomePage.xaml.cs @@ -7,10 +7,10 @@ public partial class WelcomePage : BasePage public WelcomePage() => InitializeComponent(); - async void OnAboutClicked(object sender, EventArgs e) + async void OnAboutClicked(object? sender, EventArgs e) => await Navigation.PushModalAsync(new BaseNavigationPage(new AboutPage())); - async void OnSettingsClicked(object sender, EventArgs e) + async void OnSettingsClicked(object? sender, EventArgs e) => await Navigation.PushModalAsync(new BaseNavigationPage(new SettingPage())); } } \ No newline at end of file diff --git a/samples/XCT.Sample/Resx/AppResources.Designer.cs b/samples/XCT.Sample/Resx/AppResources.Designer.cs index 1fd909ed2..150cb2cf6 100644 --- a/samples/XCT.Sample/Resx/AppResources.Designer.cs +++ b/samples/XCT.Sample/Resx/AppResources.Designer.cs @@ -10,35 +10,48 @@ namespace Xamarin.CommunityToolkit.Sample.Resx { using System; - using System.Reflection; - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [System.Diagnostics.DebuggerNonUserCodeAttribute()] - [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class AppResources { + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class AppResources { - private static System.Resources.ResourceManager resourceMan; + private static global::System.Resources.ResourceManager resourceMan; - private static System.Globalization.CultureInfo resourceCulture; + private static global::System.Globalization.CultureInfo resourceCulture; - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal AppResources() { } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Resources.ResourceManager ResourceManager { + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { get { - if (object.Equals(null, resourceMan)) { - System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Xamarin.CommunityToolkit.Sample.Resx.AppResources", typeof(AppResources).Assembly); + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Xamarin.CommunityToolkit.Sample.Resx.AppResources", typeof(AppResources).Assembly); resourceMan = temp; } return resourceMan; } } - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Globalization.CultureInfo Culture { + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -47,27 +60,48 @@ internal class AppResources { } } - internal static string ChangeLanguage { + /// + /// Looks up a localized string similar to Change language below, hit Save and see the texts in this page change. This will not affect the rest of the application, it just serves as a demo.. + /// + public static string ChangeLanguage { get { return ResourceManager.GetString("ChangeLanguage", resourceCulture); } } - internal static string English { + /// + /// Looks up a localized string similar to English. + /// + public static string English { get { return ResourceManager.GetString("English", resourceCulture); } } - internal static string Spanish { + /// + /// Looks up a localized string similar to Save. + /// + public static string Save { + get { + return ResourceManager.GetString("Save", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Spanish. + /// + public static string Spanish { get { return ResourceManager.GetString("Spanish", resourceCulture); } } - internal static string Save { + /// + /// Looks up a localized string similar to App version: {0}. + /// + public static string Version { get { - return ResourceManager.GetString("Save", resourceCulture); + return ResourceManager.GetString("Version", resourceCulture); } } } diff --git a/samples/XCT.Sample/Resx/AppResources.en.resx b/samples/XCT.Sample/Resx/AppResources.en.resx new file mode 100644 index 000000000..0d86c5b45 --- /dev/null +++ b/samples/XCT.Sample/Resx/AppResources.en.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Change language below, hit Save and see the texts in this page change. This will not affect the rest of the application, it just serves as a demo. + + + English + + + Spanish + + + Save + + + App version: {0} + + \ No newline at end of file diff --git a/samples/XCT.Sample/Resx/AppResources.es.Designer.cs b/samples/XCT.Sample/Resx/AppResources.es.Designer.cs deleted file mode 100644 index 32676c162..000000000 --- a/samples/XCT.Sample/Resx/AppResources.es.Designer.cs +++ /dev/null @@ -1,74 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Xamarin.CommunityToolkit.Sample.Resx { - using System; - using System.Reflection; - - - [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [System.Diagnostics.DebuggerNonUserCodeAttribute()] - [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class AppResources_es { - - private static System.Resources.ResourceManager resourceMan; - - private static System.Globalization.CultureInfo resourceCulture; - - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal AppResources_es() { - } - - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Resources.ResourceManager ResourceManager { - get { - if (object.Equals(null, resourceMan)) { - System.Resources.ResourceManager temp = new System.Resources.ResourceManager("Xamarin.CommunityToolkit.Sample.Resx.AppResources.es", typeof(AppResources_es).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] - internal static System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - internal static string ChangeLanguage { - get { - return ResourceManager.GetString("ChangeLanguage", resourceCulture); - } - } - - internal static string English { - get { - return ResourceManager.GetString("English", resourceCulture); - } - } - - internal static string Spanish { - get { - return ResourceManager.GetString("Spanish", resourceCulture); - } - } - - internal static string Save { - get { - return ResourceManager.GetString("Save", resourceCulture); - } - } - } -} diff --git a/samples/XCT.Sample/Resx/AppResources.es.resx b/samples/XCT.Sample/Resx/AppResources.es.resx index 91b4689d0..c9e4c0859 100644 --- a/samples/XCT.Sample/Resx/AppResources.es.resx +++ b/samples/XCT.Sample/Resx/AppResources.es.resx @@ -1,4 +1,4 @@ - + - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Change language below, hit Save and see the texts in this page change. This will not affect the rest of the application, it just serves as a demo. - + English - + Spanish - + Save + + App version: {0} + \ No newline at end of file diff --git a/samples/XCT.Sample/ViewModels/AboutViewModel.cs b/samples/XCT.Sample/ViewModels/AboutViewModel.cs index 816d2627b..b7133e5e9 100644 --- a/samples/XCT.Sample/ViewModels/AboutViewModel.cs +++ b/samples/XCT.Sample/ViewModels/AboutViewModel.cs @@ -12,13 +12,24 @@ public class AboutViewModel : BaseViewModel { readonly GitHubClient gitHubClient = new GitHubClient(new ProductHeaderValue("XamarinCommunityToolkitSample")); - RepositoryContributor[] contributors = new RepositoryContributor[0]; + RepositoryContributor[] contributors = Array.Empty(); - RepositoryContributor selectedContributor; + RepositoryContributor? selectedContributor; string emptyViewText = "Loading data..."; - ICommand selectedContributorCommand; + public AboutViewModel() + { + PageAppearingCommand = CommandFactory.Create(OnAppearing); + SelectedContributorCommand = CommandFactory.Create(async () => + { + if (SelectedContributor == null) + return; + + await Launcher.OpenAsync(SelectedContributor.HtmlUrl); + SelectedContributor = null; + }); + } public RepositoryContributor[] Contributors { @@ -26,7 +37,7 @@ public RepositoryContributor[] Contributors set => SetProperty(ref contributors, value); } - public RepositoryContributor SelectedContributor + public RepositoryContributor? SelectedContributor { get => selectedContributor; set => SetProperty(ref selectedContributor, value); @@ -38,14 +49,9 @@ public string EmptyViewText set => SetProperty(ref emptyViewText, value); } - public ICommand SelectedContributorCommand => selectedContributorCommand ??= new AsyncCommand(async () => - { - if (SelectedContributor is null) - return; + public ICommand PageAppearingCommand { get; } - await Launcher.OpenAsync(SelectedContributor.HtmlUrl); - SelectedContributor = null; - }); + public ICommand SelectedContributorCommand { get; } public async Task OnAppearing() { diff --git a/samples/XCT.Sample/ViewModels/Base/BaseGalleryViewModel.cs b/samples/XCT.Sample/ViewModels/Base/BaseGalleryViewModel.cs index dda757747..55e001563 100644 --- a/samples/XCT.Sample/ViewModels/Base/BaseGalleryViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Base/BaseGalleryViewModel.cs @@ -2,28 +2,27 @@ using System.Collections.Generic; using System.Linq; using System.Windows.Input; +using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.Sample.Models; -using Xamarin.Forms; namespace Xamarin.CommunityToolkit.Sample.ViewModels { public abstract class BaseGalleryViewModel : BaseViewModel { - ICommand filterCommand; - - protected BaseGalleryViewModel() + public BaseGalleryViewModel() { Items = CreateItems().OrderBy(x => x.Title).ToList(); Filter(); + FilterCommand = CommandFactory.Create(Filter); } - public ICommand FilterCommand => filterCommand ??= new Command(Filter); - public IReadOnlyList Items { get; } - public IEnumerable FilteredItems { get; private set; } + public ICommand FilterCommand { get; } + + public string FilterValue { private get; set; } = string.Empty; - public string FilterValue { private get; set; } + public IEnumerable FilteredItems { get; private set; } = Enumerable.Empty(); protected abstract IEnumerable CreateItems(); diff --git a/samples/XCT.Sample/ViewModels/Behaviors/BehaviorsGalleryViewModel.cs b/samples/XCT.Sample/ViewModels/Behaviors/BehaviorsGalleryViewModel.cs index 75de8c626..9c226149d 100644 --- a/samples/XCT.Sample/ViewModels/Behaviors/BehaviorsGalleryViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Behaviors/BehaviorsGalleryViewModel.cs @@ -57,6 +57,14 @@ public class BehaviorsGalleryViewModel : BaseGalleryViewModel typeof(CharactersValidationBehaviorPage), nameof(CharactersValidationBehavior), "Changes an Entry's text color when an invalid string is provided."), + new SectionModel( + typeof(ProgressBarAnimationBehaviorPage), + nameof(ProgressBarAnimationBehavior), + "Animate the progress for the ProgressBar"), + new SectionModel( + typeof(SetFocusOnEntryCompletedBehaviorPage), + nameof(SetFocusOnEntryCompletedBehavior), + "Set focus to another element when an entry is completed"), }; } } \ No newline at end of file diff --git a/samples/XCT.Sample/ViewModels/Behaviors/EventToCommandBehaviorViewModel.cs b/samples/XCT.Sample/ViewModels/Behaviors/EventToCommandBehaviorViewModel.cs index 57b8672b3..a8973056f 100644 --- a/samples/XCT.Sample/ViewModels/Behaviors/EventToCommandBehaviorViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Behaviors/EventToCommandBehaviorViewModel.cs @@ -1,20 +1,21 @@ using System.Windows.Input; -using Xamarin.Forms; +using Xamarin.CommunityToolkit.ObjectModel; namespace Xamarin.CommunityToolkit.Sample.ViewModels.Behaviors { public class EventToCommandBehaviorViewModel : BaseViewModel { - ICommand incrementCommand; - int clickCount; - public ICommand IncrementCommand => incrementCommand ??= new Command(() => ClickCount++); + public EventToCommandBehaviorViewModel() => + IncrementCommand = CommandFactory.Create(() => ClickCount++); public int ClickCount { get => clickCount; set => SetProperty(ref clickCount, value); } + + public ICommand IncrementCommand { get; } } } \ No newline at end of file diff --git a/samples/XCT.Sample/ViewModels/Behaviors/MaxLengthReachedBehaviorViewModel.cs b/samples/XCT.Sample/ViewModels/Behaviors/MaxLengthReachedBehaviorViewModel.cs index 87056411a..af0555810 100644 --- a/samples/XCT.Sample/ViewModels/Behaviors/MaxLengthReachedBehaviorViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Behaviors/MaxLengthReachedBehaviorViewModel.cs @@ -1,12 +1,12 @@ using System; using System.Windows.Input; -using Xamarin.Forms; +using Xamarin.CommunityToolkit.ObjectModel; namespace Xamarin.CommunityToolkit.Sample.ViewModels.Behaviors { public class MaxLengthReachedBehaviorViewModel : BaseViewModel { - string commandExecutions; + string commandExecutions = string.Empty; public string CommandExecutions { @@ -17,7 +17,7 @@ public string CommandExecutions public ICommand MaxLengthReachedCommand { get; } public MaxLengthReachedBehaviorViewModel() - => MaxLengthReachedCommand = new Command(OnCommandExecuted); + => MaxLengthReachedCommand = CommandFactory.Create(OnCommandExecuted); void OnCommandExecuted(string text) => CommandExecutions += string.Format("MaxLength reached with value: '{0}'.", text) + Environment.NewLine; diff --git a/samples/XCT.Sample/ViewModels/Behaviors/ProgressBarAnimationBehaviorViewModel.cs b/samples/XCT.Sample/ViewModels/Behaviors/ProgressBarAnimationBehaviorViewModel.cs new file mode 100644 index 000000000..4e950d778 --- /dev/null +++ b/samples/XCT.Sample/ViewModels/Behaviors/ProgressBarAnimationBehaviorViewModel.cs @@ -0,0 +1,31 @@ +using System.Windows.Input; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.Sample.ViewModels.Behaviors +{ + public class ProgressBarAnimationBehaviorViewModel : BaseViewModel + { + double progress; + + public ProgressBarAnimationBehaviorViewModel() + { + SetTo0Command = new Command(() => SetProgress(0)); + SetTo50Command = new Command(() => SetProgress(0.5)); + SetTo100Command = new Command(() => SetProgress(1)); + } + + public ICommand SetTo0Command { get; } + + public ICommand SetTo50Command { get; } + + public ICommand SetTo100Command { get; } + + public double Progress + { + get => progress; + set => SetProperty(ref progress, value); + } + + void SetProgress(double progress) => Progress = progress; + } +} \ No newline at end of file diff --git a/samples/XCT.Sample/ViewModels/Behaviors/UserStoppedTypingBehaviorViewModel.cs b/samples/XCT.Sample/ViewModels/Behaviors/UserStoppedTypingBehaviorViewModel.cs index a80440737..8ce3761d1 100644 --- a/samples/XCT.Sample/ViewModels/Behaviors/UserStoppedTypingBehaviorViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Behaviors/UserStoppedTypingBehaviorViewModel.cs @@ -1,14 +1,12 @@ using System; using System.Windows.Input; -using Xamarin.Forms; +using Xamarin.CommunityToolkit.ObjectModel; namespace Xamarin.CommunityToolkit.Sample.ViewModels.Behaviors { public class UserStoppedTypingBehaviorViewModel : BaseViewModel { - #region Properties - - string performedSearches; + string performedSearches = string.Empty; public string PerformedSearches { @@ -18,10 +16,8 @@ public string PerformedSearches public ICommand SearchCommand { get; } - #endregion Properties - public UserStoppedTypingBehaviorViewModel() - => SearchCommand = new Command(PerformSearch); + => SearchCommand = CommandFactory.Create(PerformSearch); void PerformSearch(string searchTerms) => PerformedSearches += string.Format("Performed search for '{0}'.", searchTerms) + Environment.NewLine; diff --git a/samples/XCT.Sample/ViewModels/Converters/ByteArrayToImageSourceViewModel.cs b/samples/XCT.Sample/ViewModels/Converters/ByteArrayToImageSourceViewModel.cs index 195c042fd..44385785e 100644 --- a/samples/XCT.Sample/ViewModels/Converters/ByteArrayToImageSourceViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Converters/ByteArrayToImageSourceViewModel.cs @@ -1,7 +1,9 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using System.Windows.Input; using Octokit; +using Xamarin.CommunityToolkit.ObjectModel; namespace Xamarin.CommunityToolkit.Sample.ViewModels.Converters { @@ -9,9 +11,9 @@ public class ByteArrayToImageSourceViewModel : BaseViewModel { readonly GitHubClient gitHubClient = new GitHubClient(new ProductHeaderValue("XamarinCommunityToolkitSample")); - byte[] avatar; + byte[]? avatar; - public byte[] Avatar + public byte[]? Avatar { get => avatar; set => SetProperty(ref avatar, value); @@ -25,6 +27,11 @@ public bool IsBusy set => SetProperty(ref isBusy, value); } + public ICommand PageAppearingCommand { get; } + + public ByteArrayToImageSourceViewModel() => + PageAppearingCommand = CommandFactory.Create(OnAppearing); + public async Task OnAppearing() { try diff --git a/samples/XCT.Sample/ViewModels/Converters/ConvertersGalleryViewModel.cs b/samples/XCT.Sample/ViewModels/Converters/ConvertersGalleryViewModel.cs index 7962ce7b6..32941eb8c 100644 --- a/samples/XCT.Sample/ViewModels/Converters/ConvertersGalleryViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Converters/ConvertersGalleryViewModel.cs @@ -85,6 +85,14 @@ public class ConvertersGalleryViewModel : BaseGalleryViewModel typeof(ListToStringConverterPage), nameof(ListToStringConverter), "A converter that allows users to convert an incoming binding that implements IEnumerable to a single string value. The Separator property is used to join the items in the IEnumerable."), + new SectionModel( + typeof(EnumToBoolConverterPage), + nameof(EnumToBoolConverter), + "A converter that allows you to convert an Enum to boolean value"), + new SectionModel( + typeof(ImageResourceConverterPage), + nameof(ImageResourceConverter), + "A converter that allows you to convert embeded ressource image id to an ImageSource"), }; } } diff --git a/samples/XCT.Sample/ViewModels/Converters/EnumToBoolConverterViewModel.cs b/samples/XCT.Sample/ViewModels/Converters/EnumToBoolConverterViewModel.cs new file mode 100644 index 000000000..098dcb240 --- /dev/null +++ b/samples/XCT.Sample/ViewModels/Converters/EnumToBoolConverterViewModel.cs @@ -0,0 +1,25 @@ +namespace Xamarin.CommunityToolkit.Sample.ViewModels.Converters +{ + public class EnumToBoolConverterViewModel : BaseViewModel + { + private IssueState selectedState = IssueState.None; + + public IssueState SelectedState + { + get => selectedState; + set => SetProperty(ref selectedState, value); + } + } + + public enum IssueState + { + None = 0, + New = 1, + Open = 2, + Waiting = 3, + Developing = 4, + WantFix = 5, + Rejected = 6, + Resolved = 7 + } +} \ No newline at end of file diff --git a/samples/XCT.Sample/ViewModels/Converters/ImageResourceConverterViewModel.cs b/samples/XCT.Sample/ViewModels/Converters/ImageResourceConverterViewModel.cs new file mode 100644 index 000000000..a19a3bcaf --- /dev/null +++ b/samples/XCT.Sample/ViewModels/Converters/ImageResourceConverterViewModel.cs @@ -0,0 +1,41 @@ +using System; +using System.Windows.Input; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.Sample.ViewModels.Converters +{ + public class ImageResourceConverterViewModel : BaseViewModel + { + const string img1 = "button.png"; + const string img2 = "logo.png"; + const string imagesPath = "Images"; + + string defaultNamespace; + + string? imageName; + + public string? ImageName + { + get => imageName; + set => SetProperty(ref imageName, value); + } + + ICommand? changeImageCommand; + + public ICommand ChangeImageCommand => changeImageCommand ??= new Command(() => + { + ImageName = (ImageName?.Equals(BuildEmbededImagePath(img1)) ?? false) ? + BuildEmbededImagePath(img2) : + BuildEmbededImagePath(img1); + }); + + public ImageResourceConverterViewModel() + { + defaultNamespace = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name; + ImageName = BuildEmbededImagePath(img1); + } + + string BuildEmbededImagePath(string imgName) + => $"{defaultNamespace}.{imagesPath}.{imgName}"; + } +} diff --git a/samples/XCT.Sample/ViewModels/Converters/IndexToArrayItemConverterViewModel.cs b/samples/XCT.Sample/ViewModels/Converters/IndexToArrayItemConverterViewModel.cs index 115b7eecb..bca6995d3 100644 --- a/samples/XCT.Sample/ViewModels/Converters/IndexToArrayItemConverterViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Converters/IndexToArrayItemConverterViewModel.cs @@ -1,9 +1,9 @@ -using System; -namespace Xamarin.CommunityToolkit.Sample.ViewModels.Converters +namespace Xamarin.CommunityToolkit.Sample.ViewModels.Converters { public class IndexToArrayItemConverterViewModel : BaseViewModel { int index; + public int Index { get => index; diff --git a/samples/XCT.Sample/ViewModels/Converters/IsNullOrEmptyConverterViewModel.cs b/samples/XCT.Sample/ViewModels/Converters/IsNullOrEmptyConverterViewModel.cs index c2efb0616..03aa7b628 100644 --- a/samples/XCT.Sample/ViewModels/Converters/IsNullOrEmptyConverterViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Converters/IsNullOrEmptyConverterViewModel.cs @@ -7,7 +7,11 @@ namespace Xamarin.CommunityToolkit.Sample.ViewModels.Converters { public class IsNullOrEmptyConverterViewModel : BaseViewModel { - public ObservableCollection DummyItemSource { get; set; } = new ObservableCollection + string? selectedItem; + + public IsNullOrEmptyConverterViewModel() => ClearSelectionCommand = new Command(() => SelectedItem = null); + + public ObservableCollection DummyItemSource { get; } = new ObservableCollection { "Dummy Item 0", "Dummy Item 1", @@ -17,16 +21,12 @@ public class IsNullOrEmptyConverterViewModel : BaseViewModel "Dummy Item 5", }; - string selectedItem; + public ICommand ClearSelectionCommand { get; } - public string SelectedItem + public string? SelectedItem { get => selectedItem; set => SetProperty(ref selectedItem, value); } - - ICommand clearSelectionCommand; - - public ICommand ClearSelectionCommand => clearSelectionCommand ??= new Command(() => SelectedItem = null); } } diff --git a/samples/XCT.Sample/ViewModels/Converters/ItemSelectedEventArgsViewModel.cs b/samples/XCT.Sample/ViewModels/Converters/ItemSelectedEventArgsViewModel.cs index 52affb3d5..101fb04ab 100644 --- a/samples/XCT.Sample/ViewModels/Converters/ItemSelectedEventArgsViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Converters/ItemSelectedEventArgsViewModel.cs @@ -15,7 +15,7 @@ public class ItemSelectedEventArgsViewModel new Person() { Id = 3, Name = "Person 3" } }; - public ICommand ItemSelectedCommand { get; private set; } = new AsyncCommand(person - => Application.Current.MainPage.DisplayAlert("Item Tapped: ", person.Name, "Cancel")); + public ICommand ItemSelectedCommand { get; } = + CommandFactory.Create(person => Application.Current.MainPage.DisplayAlert("Item Tapped: ", person?.Name, "Cancel")); } } \ No newline at end of file diff --git a/samples/XCT.Sample/ViewModels/Converters/ItemTappedEventArgsViewModel.cs b/samples/XCT.Sample/ViewModels/Converters/ItemTappedEventArgsViewModel.cs index 7f53249d1..eedfb6995 100644 --- a/samples/XCT.Sample/ViewModels/Converters/ItemTappedEventArgsViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Converters/ItemTappedEventArgsViewModel.cs @@ -7,22 +7,21 @@ namespace Xamarin.CommunityToolkit.Sample.ViewModels.Converters { public class ItemTappedEventArgsViewModel { - public IEnumerable Items { get; } = - new List() - { - new Person() { Id = 1, Name = "Person 1" }, - new Person() { Id = 2, Name = "Person 2" }, - new Person() { Id = 3, Name = "Person 3" } - }; + public IEnumerable Items { get; } = new List() + { + new Person() { Id = 1, Name = "Person 1" }, + new Person() { Id = 2, Name = "Person 2" }, + new Person() { Id = 3, Name = "Person 3" } + }; - public ICommand ItemTappedCommand { get; private set; } = new AsyncCommand(person - => Application.Current.MainPage.DisplayAlert("Item Tapped: ", person.Name, "Cancel")); + public ICommand ItemTappedCommand { get; } = + CommandFactory.Create(person => Application.Current.MainPage.DisplayAlert("Item Tapped: ", person?.Name, "Cancel")); } public class Person { public int Id { get; set; } - public string Name { get; set; } + public string Name { get; set; } = string.Empty; } } \ No newline at end of file diff --git a/samples/XCT.Sample/ViewModels/Converters/ListIsNullOrEmptyViewModel.cs b/samples/XCT.Sample/ViewModels/Converters/ListIsNullOrEmptyViewModel.cs index 392a6415a..051593e0a 100644 --- a/samples/XCT.Sample/ViewModels/Converters/ListIsNullOrEmptyViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Converters/ListIsNullOrEmptyViewModel.cs @@ -1,4 +1,5 @@ -using Xamarin.CommunityToolkit.ObjectModel; +using Xamarin.CommunityToolkit.Helpers; +using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.Forms; namespace Xamarin.CommunityToolkit.Sample.ViewModels.Converters @@ -9,7 +10,7 @@ public class ListIsNullOrEmptyViewModel : BaseViewModel public ListIsNullOrEmptyViewModel() { - AddItemCommand = new Command(() => + AddItemCommand = CommandFactory.Create(() => { Items.Add(new Person { @@ -17,7 +18,7 @@ public ListIsNullOrEmptyViewModel() Name = $"Person {Items.Count}" }); }); - RemoveItemCommand = new Command(() => Items.RemoveAt(0)); + RemoveItemCommand = CommandFactory.Create(() => Items.RemoveAt(0)); // ListIsNullOrEmptyConvertor needs to know that Items are updated Items.CollectionChanged += (sender, e) => OnPropertyChanged(nameof(Items)); diff --git a/samples/XCT.Sample/ViewModels/Converters/VariableMultiValueConverterViewModel.cs b/samples/XCT.Sample/ViewModels/Converters/VariableMultiValueConverterViewModel.cs index 6606443d2..5b1feabf6 100644 --- a/samples/XCT.Sample/ViewModels/Converters/VariableMultiValueConverterViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Converters/VariableMultiValueConverterViewModel.cs @@ -1,5 +1,4 @@ -using System; -namespace Xamarin.CommunityToolkit.Sample.ViewModels.Converters +namespace Xamarin.CommunityToolkit.Sample.ViewModels.Converters { public class VariableMultiValueConverterViewModel : BaseViewModel { diff --git a/samples/XCT.Sample/ViewModels/Effects/EffectsGalleryViewModel.cs b/samples/XCT.Sample/ViewModels/Effects/EffectsGalleryViewModel.cs index 51b400743..38a70cdb3 100644 --- a/samples/XCT.Sample/ViewModels/Effects/EffectsGalleryViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Effects/EffectsGalleryViewModel.cs @@ -33,6 +33,16 @@ public class EffectsGalleryViewModel : BaseGalleryViewModel typeof(TouchEffectPage), nameof(TouchEffect), "The TouchEffect is an effect that allows changing the view's appearance depending on the touch state (normal, pressed, hovered). Also, it allows to handle long presses."), + + new SectionModel( + typeof(LifeCycleEffectPage), + nameof(LifecycleEffect), + "The LifeCycle is an effect that allows you to know when a control or layout is loaded or/and unloaded in the screen and perform actions based on that."), + + new SectionModel( + typeof(ShadowEffectPage), + nameof(ShadowEffect), + "The ShadowEffect allows all views to display shadow.") }; } } \ No newline at end of file diff --git a/samples/XCT.Sample/ViewModels/Extensions/ExtensionsGalleryViewModel.cs b/samples/XCT.Sample/ViewModels/Extensions/ExtensionsGalleryViewModel.cs new file mode 100644 index 000000000..e254ffd77 --- /dev/null +++ b/samples/XCT.Sample/ViewModels/Extensions/ExtensionsGalleryViewModel.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using Xamarin.CommunityToolkit.Extensions; +using Xamarin.CommunityToolkit.Sample.Models; +using Xamarin.CommunityToolkit.Sample.Pages.Effects; + +namespace Xamarin.CommunityToolkit.Sample.ViewModels.Effects +{ + public class ExtensionsGalleryViewModel : BaseGalleryViewModel + { + protected override IEnumerable CreateItems() => new[] + { + new SectionModel( + typeof(ImageResourceExtensionPage), + nameof(ImageResourceExtension), + "A XAML extension that helps to display images from embedded resources"), + }; + } +} diff --git a/samples/XCT.Sample/ViewModels/Markup/SearchViewModel.cs b/samples/XCT.Sample/ViewModels/Markup/SearchViewModel.cs index e9c21cfed..62e2b24c9 100644 --- a/samples/XCT.Sample/ViewModels/Markup/SearchViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Markup/SearchViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using System.Windows.Input; using Xamarin.Essentials; @@ -8,8 +9,6 @@ namespace Xamarin.CommunityToolkit.Sample.ViewModels.Markup { public class SearchViewModel : BaseViewModel { - ICommand backCommand, likeCommand, openTwitterSearchCommand, openHelpCommand; - public string SearchText { get; set; } public List SearchResults { get; set; } @@ -54,17 +53,24 @@ public SearchViewModel() }, } }; + + BackCommand = new RelayCommand(Back); + LikeCommand = new RelayCommand(Like); + OpenTwitterSearchCommand = new RelayCommandAsync(OpenTwitterSearch); + OpenHelpCommand = new RelayCommandAsync(OpenHelp); } - public ICommand BackCommand => backCommand ??= new RelayCommand(Back); + public ICommand BackCommand { get; } - public ICommand LikeCommand => likeCommand ??= new RelayCommand(Like); + public ICommand LikeCommand { get; } - public ICommand OpenTwitterSearchCommand => openTwitterSearchCommand ??= new RelayCommandAsync(OpenTwitterSearch); + public ICommand OpenTwitterSearchCommand { get; } - public ICommand OpenHelpCommand => openHelpCommand ??= new RelayCommandAsync(OpenHelp); + public ICommand OpenHelpCommand { get; } - void Back() { } + void Back() + { + } void Like(Tweet tweet) => tweet.IsLikedByMe = !tweet.IsLikedByMe; @@ -74,18 +80,18 @@ public SearchViewModel() public class Tweet : BaseViewModel { - public string AuthorImage { get; set; } + public string AuthorImage { get; set; } = string.Empty; - public string Header { get; set; } + public string Header { get; set; } = string.Empty; - public List Body { get; set; } + public List Body { get; set; } = new List(); public bool IsLikedByMe { get; set; } } public class TextFragment { - public string Text { get; set; } + public string Text { get; set; } = string.Empty; public bool IsMatch { get; set; } } diff --git a/samples/XCT.Sample/ViewModels/SettingViewModel.cs b/samples/XCT.Sample/ViewModels/SettingViewModel.cs index a9b57200e..82578e00b 100644 --- a/samples/XCT.Sample/ViewModels/SettingViewModel.cs +++ b/samples/XCT.Sample/ViewModels/SettingViewModel.cs @@ -1,27 +1,36 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Windows.Input; using Xamarin.CommunityToolkit.Helpers; +using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.Sample.Models; using Xamarin.CommunityToolkit.Sample.Resx; -using Xamarin.Forms; +using Xamarin.Essentials; namespace Xamarin.CommunityToolkit.Sample.ViewModels { public class SettingViewModel : BaseViewModel { - public SettingViewModel() => LoadLanguages(); + IList supportedLanguages = Enumerable.Empty().ToList(); - IList supportedLanguages; + Language selectedLanguage = new Language(AppResources.English, "en"); - public IList SupportedLanguages + public SettingViewModel() { - get => supportedLanguages; - private set => SetProperty(ref supportedLanguages, value); + LoadLanguages(); + + ChangeLanguageCommand = CommandFactory.Create(() => + { + LocalizationResourceManager.Current.CurrentCulture = CultureInfo.GetCultureInfo(SelectedLanguage.CI); + LoadLanguages(); + }); } - Language selectedLanguage; + public LocalizedString AppVersion { get; } = new LocalizedString(() => string.Format(AppResources.Version, AppInfo.VersionString)); + + public ICommand ChangeLanguageCommand { get; } public Language SelectedLanguage { @@ -29,13 +38,11 @@ public Language SelectedLanguage set => SetProperty(ref selectedLanguage, value); } - ICommand changeLanguageCommand; - - public ICommand ChangeLanguageCommand => changeLanguageCommand ??= new Command(() => + public IList SupportedLanguages { - LocalizationResourceManager.Current.SetCulture(CultureInfo.GetCultureInfo(SelectedLanguage.CI)); - LoadLanguages(); - }); + get => supportedLanguages; + private set => SetProperty(ref supportedLanguages, value); + } void LoadLanguages() { diff --git a/samples/XCT.Sample/ViewModels/Views/BadgeViewViewModel.cs b/samples/XCT.Sample/ViewModels/Views/BadgeViewViewModel.cs index 0aea46870..c95155981 100644 --- a/samples/XCT.Sample/ViewModels/Views/BadgeViewViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Views/BadgeViewViewModel.cs @@ -1,5 +1,5 @@ using System.Windows.Input; -using Xamarin.Forms; +using Xamarin.CommunityToolkit.ObjectModel; namespace Xamarin.CommunityToolkit.Sample.ViewModels.Views { @@ -7,12 +7,17 @@ public class BadgeViewViewModel : BaseViewModel { int counter; - public BadgeViewViewModel() => Counter = 3; + public BadgeViewViewModel() + { + Counter = 3; + + IncreaseCommand = CommandFactory.Create(Increase); + DecreaseCommand = CommandFactory.Create(Decrease); + } public int Counter { get => counter; - set { counter = value; @@ -20,9 +25,9 @@ public int Counter } } - public ICommand IncreaseCommand => new Command(Increase); + public ICommand IncreaseCommand { get; } - public ICommand DecreaseCommand => new Command(Decrease); + public ICommand DecreaseCommand { get; } void Increase() => Counter++; diff --git a/samples/XCT.Sample/ViewModels/Views/ExpanderViewModel.cs b/samples/XCT.Sample/ViewModels/Views/ExpanderViewModel.cs index 98e1d5e2d..9281f689f 100644 --- a/samples/XCT.Sample/ViewModels/Views/ExpanderViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Views/ExpanderViewModel.cs @@ -1,21 +1,23 @@ using System.Windows.Input; -using Xamarin.Forms; +using Xamarin.CommunityToolkit.ObjectModel; namespace Xamarin.CommunityToolkit.Sample.ViewModels.Views { public partial class ExpanderViewModel : BaseViewModel { - ICommand command; - - public ICommand Command => command ??= new Command(p => + public ExpanderViewModel() { - var sender = (Item)p; - if (!sender.IsExpanded) - return; + Command = CommandFactory.Create(sender => + { + if (!sender.IsExpanded) + return; + + foreach (var item in Items) + item.IsExpanded = sender == item; + }); + } - foreach (var item in Items) - item.IsExpanded = sender == item; - }); + public ICommand Command { get; } public Item[] Items { get; } = new Item[] { @@ -44,7 +46,7 @@ public partial class ExpanderViewModel : BaseViewModel public sealed class Item : BaseViewModel { - string name; + string name = string.Empty; bool isExpanded; bool isEnabled = true; diff --git a/samples/XCT.Sample/ViewModels/Views/PopupControlViewModel.cs b/samples/XCT.Sample/ViewModels/Views/PopupControlViewModel.cs new file mode 100644 index 000000000..eec92643c --- /dev/null +++ b/samples/XCT.Sample/ViewModels/Views/PopupControlViewModel.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Input; +using Xamarin.CommunityToolkit.Extensions; +using Xamarin.CommunityToolkit.ObjectModel; +using Xamarin.CommunityToolkit.Sample.Models; +using Xamarin.CommunityToolkit.Sample.Pages.Views.Popups; +using Xamarin.CommunityToolkit.UI.Views; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.Sample.ViewModels.Views +{ + public class PopupGalleryViewModel + { + public PopupGalleryViewModel() + { + DisplayPopup = CommandFactory.Create(OnDisplayPopup); + } + + INavigation Navigation => Application.Current.MainPage.Navigation; + + public IEnumerable Examples { get; } = new[] + { + new SectionModel(typeof(SimplePopup), "Simple Popup", Color.Red, "Displays a basic popup centered on the screen"), + new SectionModel(typeof(PopupPositionPage), "Custom Positioning Popup", Color.Red, "Displays a basic popup anywhere on the screen using VerticalOptions and HorizontalOptions"), + new SectionModel(typeof(ButtonPopup), "Popup With 1 Button", Color.Red, "Displays a basic popup with a confirm button"), + new SectionModel(typeof(MultipleButtonPopup), "Popup With Multiple Buttons", Color.Red, "Displays a basic popup with a cancel and confirm button"), + new SectionModel(typeof(NoLightDismissPopup), "Simple Popup Without Light Dismiss", Color.Red, "Displays a basic popup but does not allow the user to close it if they tap outside of the popup. In other words the LightDismiss is set to false."), + new SectionModel(typeof(ToggleSizePopup), "Toggle Size Popup", Color.Red, "Displays a popup that can have it's size updated by pressing a button"), + new SectionModel(typeof(TransparentPopup), "Transparent Popup", Color.Red, "Displays a popup with a transparent background"), + new SectionModel(typeof(PopupAnchorPage), "Anchor Popup", Color.Red, "Popups can be anchored to other view's on the screen"), + new SectionModel(typeof(OpenedEventSimplePopup), "Opened Event Popup", Color.Red, "Popup with opened event"), + new SectionModel(typeof(ReturnResultPopup), "Return Result Popup", Color.Red, "A popup that returns a string message when dismissed"), + new SectionModel(typeof(XamlBindingPopup), "Xaml Binding Popup", Color.Red, "A simple popup that uses XAML BindingContext"), + new SectionModel(typeof(CsharpBindingPopup), "C# Binding Popup", Color.Red, "A simple popup that uses C# BindingContext") + }.OrderBy(x => x.Title); + + public ICommand DisplayPopup { get; } + + async Task OnDisplayPopup(Type? popupType) + { + var view = (VisualElement)Activator.CreateInstance(popupType); + + if (view is Popup popup) + { + var result = await Navigation.ShowPopupAsync(popup); + await Application.Current.MainPage.DisplayAlert("Popup Result", result, "OKAY"); + } + else if (view is BasePopup basePopup) + { + Navigation.ShowPopup(basePopup); + } + else if (view is Page page) + { + await Navigation.PushAsync(page); + } + } + } +} diff --git a/samples/XCT.Sample/ViewModels/Views/Popups/CsharpBindingPopupViewModel.cs b/samples/XCT.Sample/ViewModels/Views/Popups/CsharpBindingPopupViewModel.cs new file mode 100644 index 000000000..ce53e9799 --- /dev/null +++ b/samples/XCT.Sample/ViewModels/Views/Popups/CsharpBindingPopupViewModel.cs @@ -0,0 +1,9 @@ +namespace Xamarin.CommunityToolkit.Sample.ViewModels.Views.Popups +{ + public class CsharpBindingPopupViewModel + { + public string Title { get; } = "Xaml Binding Popup"; + + public string Message { get; } = "This is a native popup with a Xamarin.Forms View being rendered. The behaviors of the popup will confirm to 100% native look and feel, but still allows you to use your Xamarin.Forms controls."; + } +} diff --git a/samples/XCT.Sample/ViewModels/Views/Popups/PopupAnchorViewModel.cs b/samples/XCT.Sample/ViewModels/Views/Popups/PopupAnchorViewModel.cs new file mode 100644 index 000000000..440bf9c3d --- /dev/null +++ b/samples/XCT.Sample/ViewModels/Views/Popups/PopupAnchorViewModel.cs @@ -0,0 +1,26 @@ +using System.Windows.Input; +using Xamarin.CommunityToolkit.Extensions; +using Xamarin.CommunityToolkit.Sample.Pages.Views.Popups; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.Sample.ViewModels.Views.Popups +{ + public class PopupAnchorViewModel + { + public PopupAnchorViewModel() + { + ShowPopup = new Command(OnShowPopup); + } + + INavigation Navigation => Application.Current.MainPage.Navigation; + + public ICommand ShowPopup { get; } + + void OnShowPopup(View anchor) + { + var popup = new TransparentPopup(); + popup.Anchor = anchor; + Navigation.ShowPopup(popup); + } + } +} diff --git a/samples/XCT.Sample/ViewModels/Views/Popups/PopupPositionViewModel.cs b/samples/XCT.Sample/ViewModels/Views/Popups/PopupPositionViewModel.cs new file mode 100644 index 000000000..4e12e874d --- /dev/null +++ b/samples/XCT.Sample/ViewModels/Views/Popups/PopupPositionViewModel.cs @@ -0,0 +1,79 @@ +using System.Windows.Input; +using Xamarin.CommunityToolkit.Extensions; +using Xamarin.CommunityToolkit.Sample.Pages.Views.Popups; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.Sample.ViewModels.Views.Popups +{ + public class PopupPositionViewModel + { + public PopupPositionViewModel() + { + DisplayPopup = new Command(OnDisplayPopup); + } + + INavigation Navigation => Application.Current.MainPage.Navigation; + + public ICommand DisplayPopup { get; } + + void OnDisplayPopup(PopupPosition position) + { + var popup = new SimplePopup(); + + switch (position) + { + case PopupPosition.TopLeft: + popup.VerticalOptions = new LayoutOptions(LayoutAlignment.Start, true); + popup.HorizontalOptions = new LayoutOptions(LayoutAlignment.Start, true); + break; + case PopupPosition.Top: + popup.VerticalOptions = new LayoutOptions(LayoutAlignment.Start, true); + popup.HorizontalOptions = new LayoutOptions(LayoutAlignment.Center, true); + break; + case PopupPosition.TopRight: + popup.VerticalOptions = new LayoutOptions(LayoutAlignment.Start, true); + popup.HorizontalOptions = new LayoutOptions(LayoutAlignment.End, true); + break; + case PopupPosition.Left: + popup.VerticalOptions = new LayoutOptions(LayoutAlignment.Center, true); + popup.HorizontalOptions = new LayoutOptions(LayoutAlignment.Start, true); + break; + case PopupPosition.Center: + popup.VerticalOptions = new LayoutOptions(LayoutAlignment.Center, true); + popup.HorizontalOptions = new LayoutOptions(LayoutAlignment.Center, true); + break; + case PopupPosition.Right: + popup.VerticalOptions = new LayoutOptions(LayoutAlignment.Center, true); + popup.HorizontalOptions = new LayoutOptions(LayoutAlignment.End, true); + break; + case PopupPosition.BottomLeft: + popup.VerticalOptions = new LayoutOptions(LayoutAlignment.End, true); + popup.HorizontalOptions = new LayoutOptions(LayoutAlignment.Start, true); + break; + case PopupPosition.Bottom: + popup.VerticalOptions = new LayoutOptions(LayoutAlignment.End, true); + popup.HorizontalOptions = new LayoutOptions(LayoutAlignment.Center, true); + break; + case PopupPosition.BottomRight: + popup.VerticalOptions = new LayoutOptions(LayoutAlignment.End, true); + popup.HorizontalOptions = new LayoutOptions(LayoutAlignment.End, true); + break; + } + + Navigation.ShowPopup(popup); + } + + public enum PopupPosition + { + TopLeft = 0, + Top = 1, + TopRight = 2, + Left = 3, + Center = 4, + Right = 5, + BottomLeft = 6, + Bottom = 7, + BottomRight = 8 + } + } +} diff --git a/samples/XCT.Sample/ViewModels/Views/Popups/XamlBindingPopupViewModel.cs b/samples/XCT.Sample/ViewModels/Views/Popups/XamlBindingPopupViewModel.cs new file mode 100644 index 000000000..421f699dc --- /dev/null +++ b/samples/XCT.Sample/ViewModels/Views/Popups/XamlBindingPopupViewModel.cs @@ -0,0 +1,9 @@ +namespace Xamarin.CommunityToolkit.Sample.ViewModels.Views.Popups +{ + public class XamlBindingPopupViewModel + { + public string Title { get; } = "Xaml Binding Popup"; + + public string Message { get; } = "This is a native popup with a Xamarin.Forms View being rendered. The behaviors of the popup will confirm to 100% native look and feel, but still allows you to use your Xamarin.Forms controls."; + } +} diff --git a/samples/XCT.Sample/ViewModels/Views/StateLayoutViewModel.cs b/samples/XCT.Sample/ViewModels/Views/StateLayoutViewModel.cs index 68b673a2c..f856df57d 100644 --- a/samples/XCT.Sample/ViewModels/Views/StateLayoutViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Views/StateLayoutViewModel.cs @@ -1,15 +1,13 @@ using System.Threading.Tasks; using System.Windows.Input; +using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.CommunityToolkit.UI.Views; -using Xamarin.Forms; namespace Xamarin.CommunityToolkit.Sample.ViewModels.Views { public class StateLayoutViewModel : BaseViewModel { - ICommand fullscreenLoadingCommand; - ICommand cycleStatesCommand; - string customState; + string customState = string.Empty; LayoutState currentState; LayoutState mainState; @@ -31,28 +29,19 @@ public string CustomState set => SetProperty(ref customState, value); } - public ICommand FullscreenLoadingCommand - { - get => fullscreenLoadingCommand; - set => SetProperty(ref fullscreenLoadingCommand, value); - } + public ICommand FullscreenLoadingCommand { get; } - public ICommand CycleStatesCommand - { - get => cycleStatesCommand; - set => SetProperty(ref cycleStatesCommand, value); - } + public ICommand CycleStatesCommand { get; } public StateLayoutViewModel() { - FullscreenLoadingCommand = new Command(async (x) => + FullscreenLoadingCommand = CommandFactory.Create(async () => { MainState = LayoutState.Loading; await Task.Delay(2000); MainState = LayoutState.None; }); - - CycleStatesCommand = new Command(async (x) => await CycleStates()); + CycleStatesCommand = CommandFactory.Create(CycleStates); } async Task CycleStates() diff --git a/samples/XCT.Sample/ViewModels/Views/TabItemsSourceViewModel.cs b/samples/XCT.Sample/ViewModels/Views/TabItemsSourceViewModel.cs index 8e08baa8c..4d890f101 100644 --- a/samples/XCT.Sample/ViewModels/Views/TabItemsSourceViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Views/TabItemsSourceViewModel.cs @@ -1,16 +1,22 @@ using System.Collections.ObjectModel; using System.Windows.Input; +using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.Forms; namespace Xamarin.CommunityToolkit.Sample.ViewModels.Views { public class Monkey { - public string Index { get; set; } - public string Name { get; set; } - public string Location { get; set; } - public string Details { get; set; } - public string Image { get; set; } + public string Index { get; set; } = string.Empty; + + public string Name { get; set; } = string.Empty; + + public string Location { get; set; } = string.Empty; + + public string Details { get; set; } = string.Empty; + + public string Image { get; set; } = string.Empty; + public Color Color { get; set; } } @@ -19,114 +25,111 @@ public class TabItemsSourceViewModel : BaseViewModel public TabItemsSourceViewModel() { LoadMonkeys(); + + ClearDataCommand = CommandFactory.Create(ClearData); + UpdateDataCommand = CommandFactory.Create(UpdateData); } - public ObservableCollection Monkeys { get; set; } + public ObservableCollection Monkeys { get; } = LoadMonkeys(); - public ICommand ClearDataCommand => new Command(ClearData); + public ICommand ClearDataCommand { get; } - public ICommand UpdateDataCommand => new Command(UpdateData); + public ICommand UpdateDataCommand { get; } - void LoadMonkeys() + static ObservableCollection LoadMonkeys() => new ObservableCollection { - Monkeys = new ObservableCollection + new Monkey { - new Monkey - { - Index = "0", - Name = "Baboon", - Location = "Africa & Asia", - Details = "Baboons are African and Arabian Old World monkeys belonging to the genus Papio, part of the subfamily Cercopithecinae.", - Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Papio_anubis_%28Serengeti%2C_2009%29.jpg/200px-Papio_anubis_%28Serengeti%2C_2009%29.jpg", - Color = Color.LightSalmon - }, - - new Monkey - { - Index = "1", - Name = "Capuchin Monkey", - Location = "Central & South America", - Details = "The capuchin monkeys are New World monkeys of the subfamily Cebinae. Prior to 2011, the subfamily contained only a single genus, Cebus.", - Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/4/40/Capuchin_Costa_Rica.jpg/200px-Capuchin_Costa_Rica.jpg", - Color = Color.LightBlue - }, - - new Monkey - { - Index = "2", - Name = "Blue Monkey", - Location = "Central and East Africa", - Details = "The blue monkey or diademed monkey is a species of Old World monkey native to Central and East Africa, ranging from the upper Congo River basin east to the East African Rift and south to northern Angola and Zambia", - Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/8/83/BlueMonkey.jpg/220px-BlueMonkey.jpg", - Color = Color.LightSlateGray - }, - - new Monkey - { - Index = "3", - Name = "Squirrel Monkey", - Location = "Central & South America", - Details = "The squirrel monkeys are the New World monkeys of the genus Saimiri. They are the only genus in the subfamily Saimirinae. The name of the genus Saimiri is of Tupi origin, and was also used as an English name by early researchers.", - Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/2/20/Saimiri_sciureus-1_Luc_Viatour.jpg/220px-Saimiri_sciureus-1_Luc_Viatour.jpg", - Color = Color.Chocolate - }, - - new Monkey - { - Index = "4", - Name = "Golden Lion Tamarin", - Location = "Brazil", - Details = "The golden lion tamarin also known as the golden marmoset, is a small New World monkey of the family Callitrichidae.", - Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/8/87/Golden_lion_tamarin_portrait3.jpg/220px-Golden_lion_tamarin_portrait3.jpg", - Color = Color.Violet - }, - - new Monkey - { - Index = "5", - Name = "Howler Monkey", - Location = "South America", - Details = "Howler monkeys are among the largest of the New World monkeys. Fifteen species are currently recognised. Previously classified in the family Cebidae, they are now placed in the family Atelidae.", - Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/0/0d/Alouatta_guariba.jpg/200px-Alouatta_guariba.jpg", - Color = Color.Aqua - }, - - new Monkey - { - Index = "6", - Name = "Japanese Macaque", - Location = "Japan", - Details = "The Japanese macaque, is a terrestrial Old World monkey species native to Japan. They are also sometimes known as the snow monkey because they live in areas where snow covers the ground for months each", - Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Macaca_fuscata_fuscata1.jpg/220px-Macaca_fuscata_fuscata1.jpg", - Color = Color.OrangeRed - }, - - new Monkey - { - Index = "7", - Name = "Mandrill", - Location = "Southern Cameroon, Gabon, Equatorial Guinea, and Congo", - Details = "The mandrill is a primate of the Old World monkey family, closely related to the baboons and even more closely to the drill. It is found in southern Cameroon, Gabon, Equatorial Guinea, and Congo.", - Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/7/75/Mandrill_at_san_francisco_zoo.jpg/220px-Mandrill_at_san_francisco_zoo.jpg", - Color = Color.MediumPurple - }, - - new Monkey - { - Index = "8", - Name = "Proboscis Monkey", - Location = "Borneo", - Details = "The proboscis monkey or long-nosed monkey, known as the bekantan in Malay, is a reddish-brown arboreal Old World monkey that is endemic to the south-east Asian island of Borneo.", - Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/e/e5/Proboscis_Monkey_in_Borneo.jpg/250px-Proboscis_Monkey_in_Borneo.jpg", - Color = Color.Pink - } - }; - } + Index = "0", + Name = "Baboon", + Location = "Africa & Asia", + Details = "Baboons are African and Arabian Old World monkeys belonging to the genus Papio, part of the subfamily Cercopithecinae.", + Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Papio_anubis_%28Serengeti%2C_2009%29.jpg/200px-Papio_anubis_%28Serengeti%2C_2009%29.jpg", + Color = Color.LightSalmon + }, + + new Monkey + { + Index = "1", + Name = "Capuchin Monkey", + Location = "Central & South America", + Details = "The capuchin monkeys are New World monkeys of the subfamily Cebinae. Prior to 2011, the subfamily contained only a single genus, Cebus.", + Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/4/40/Capuchin_Costa_Rica.jpg/200px-Capuchin_Costa_Rica.jpg", + Color = Color.LightBlue + }, + + new Monkey + { + Index = "2", + Name = "Blue Monkey", + Location = "Central and East Africa", + Details = "The blue monkey or diademed monkey is a species of Old World monkey native to Central and East Africa, ranging from the upper Congo River basin east to the East African Rift and south to northern Angola and Zambia", + Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/8/83/BlueMonkey.jpg/220px-BlueMonkey.jpg", + Color = Color.LightSlateGray + }, + + new Monkey + { + Index = "3", + Name = "Squirrel Monkey", + Location = "Central & South America", + Details = "The squirrel monkeys are the New World monkeys of the genus Saimiri. They are the only genus in the subfamily Saimirinae. The name of the genus Saimiri is of Tupi origin, and was also used as an English name by early researchers.", + Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/2/20/Saimiri_sciureus-1_Luc_Viatour.jpg/220px-Saimiri_sciureus-1_Luc_Viatour.jpg", + Color = Color.Chocolate + }, + + new Monkey + { + Index = "4", + Name = "Golden Lion Tamarin", + Location = "Brazil", + Details = "The golden lion tamarin also known as the golden marmoset, is a small New World monkey of the family Callitrichidae.", + Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/8/87/Golden_lion_tamarin_portrait3.jpg/220px-Golden_lion_tamarin_portrait3.jpg", + Color = Color.Violet + }, + + new Monkey + { + Index = "5", + Name = "Howler Monkey", + Location = "South America", + Details = "Howler monkeys are among the largest of the New World monkeys. Fifteen species are currently recognised. Previously classified in the family Cebidae, they are now placed in the family Atelidae.", + Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/0/0d/Alouatta_guariba.jpg/200px-Alouatta_guariba.jpg", + Color = Color.Aqua + }, - void ClearData() - { - Monkeys.Clear(); - } + new Monkey + { + Index = "6", + Name = "Japanese Macaque", + Location = "Japan", + Details = "The Japanese macaque, is a terrestrial Old World monkey species native to Japan. They are also sometimes known as the snow monkey because they live in areas where snow covers the ground for months each", + Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/c/c1/Macaca_fuscata_fuscata1.jpg/220px-Macaca_fuscata_fuscata1.jpg", + Color = Color.OrangeRed + }, + + new Monkey + { + Index = "7", + Name = "Mandrill", + Location = "Southern Cameroon, Gabon, Equatorial Guinea, and Congo", + Details = "The mandrill is a primate of the Old World monkey family, closely related to the baboons and even more closely to the drill. It is found in southern Cameroon, Gabon, Equatorial Guinea, and Congo.", + Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/7/75/Mandrill_at_san_francisco_zoo.jpg/220px-Mandrill_at_san_francisco_zoo.jpg", + Color = Color.MediumPurple + }, + + new Monkey + { + Index = "8", + Name = "Proboscis Monkey", + Location = "Borneo", + Details = "The proboscis monkey or long-nosed monkey, known as the bekantan in Malay, is a reddish-brown arboreal Old World monkey that is endemic to the south-east Asian island of Borneo.", + Image = "http://upload.wikimedia.org/wikipedia/commons/thumb/e/e5/Proboscis_Monkey_in_Borneo.jpg/250px-Proboscis_Monkey_in_Borneo.jpg", + Color = Color.Pink + } + }; + + void ClearData() => Monkeys.Clear(); void UpdateData() { diff --git a/samples/XCT.Sample/ViewModels/Views/TabViewViewModel.cs b/samples/XCT.Sample/ViewModels/Views/TabViewViewModel.cs index 52d69e160..1bbdf8fb6 100644 --- a/samples/XCT.Sample/ViewModels/Views/TabViewViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Views/TabViewViewModel.cs @@ -42,7 +42,10 @@ public class TabViewViewModel : BaseGalleryViewModel "Using TabViewItem VisualStates"), new SectionModel(typeof(NoContentPage), "Tab without Content", - "Only the TabStrip is visible") + "Only the TabStrip is visible"), + + new SectionModel(typeof(LazyTabPage), "LazyLoadingTab", + "See how you can implement LazyViews that are loaded just when you navigate to them"), }; } } \ No newline at end of file diff --git a/samples/XCT.Sample/ViewModels/Views/Tabs/LazyTestViewModel.cs b/samples/XCT.Sample/ViewModels/Views/Tabs/LazyTestViewModel.cs new file mode 100644 index 000000000..e3f6c5828 --- /dev/null +++ b/samples/XCT.Sample/ViewModels/Views/Tabs/LazyTestViewModel.cs @@ -0,0 +1,27 @@ +using Xamarin.CommunityToolkit.ObjectModel; + +namespace Xamarin.CommunityToolkit.Sample.ViewModels.Views.Tabs +{ + sealed class LazyTestViewModel : ObservableObject + { + public static LazyTestViewModel Current { get; } = new LazyTestViewModel(); + + string title = string.Empty; + + public string Title + { + get => title; + set => SetProperty(ref title, value); + } + + bool loaded; + + public bool Loaded + { + get => loaded; + set => SetProperty(ref loaded, value); + } + + public LazyTestViewModel() => Title = "Lazy Tab Sample"; + } +} diff --git a/samples/XCT.Sample/ViewModels/Views/Tabs/NormalTestViewModel.cs b/samples/XCT.Sample/ViewModels/Views/Tabs/NormalTestViewModel.cs new file mode 100644 index 000000000..81c51d12d --- /dev/null +++ b/samples/XCT.Sample/ViewModels/Views/Tabs/NormalTestViewModel.cs @@ -0,0 +1,17 @@ +using Xamarin.CommunityToolkit.ObjectModel; + +namespace Xamarin.CommunityToolkit.Sample.ViewModels.Views.Tabs +{ + sealed class NormalTestViewModel : ObservableObject + { + public static NormalTestViewModel Current { get; } = new NormalTestViewModel(); + + string loadedViews = string.Empty; + + public string LoadedViews + { + get => loadedViews; + set => SetProperty(ref loadedViews, value); + } + } +} diff --git a/samples/XCT.Sample/ViewModels/Views/ViewsGalleryViewModel.cs b/samples/XCT.Sample/ViewModels/Views/ViewsGalleryViewModel.cs index a469bdc9f..72caebe65 100644 --- a/samples/XCT.Sample/ViewModels/Views/ViewsGalleryViewModel.cs +++ b/samples/XCT.Sample/ViewModels/Views/ViewsGalleryViewModel.cs @@ -48,7 +48,10 @@ public class ViewsGalleryViewModel : BaseGalleryViewModel "A control to display a set of tabs and their respective content."), new SectionModel(typeof(UniformGridPage), "UniformGrid", - "The UniformGrid is just like the Grid, with all rows and columns will have the same size.") + "The UniformGrid is just like the Grid, with all rows and columns will have the same size."), + + new SectionModel(typeof(PopupGalleryPage), "Popup", + "The popup control renders native popups from the shared code. This page demonstrates a variety of different techniques for displaying native popups.") }; } } diff --git a/samples/XCT.Sample/Xamarin.CommunityToolkit.Sample.csproj b/samples/XCT.Sample/Xamarin.CommunityToolkit.Sample.csproj index af5548634..eb1854e4f 100644 --- a/samples/XCT.Sample/Xamarin.CommunityToolkit.Sample.csproj +++ b/samples/XCT.Sample/Xamarin.CommunityToolkit.Sample.csproj @@ -12,6 +12,7 @@ + @@ -19,10 +20,32 @@ - + + + + True + True + AppResources.resx + + + + + + PublicResXFileCodeGenerator + AppResources.resx + + + PublicResXFileCodeGenerator + AppResources.resx + + + PublicResXFileCodeGenerator + AppResources.Designer.cs + + + - diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/CharactersValidationBehavior_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/CharactersValidationBehavior_Tests.cs index d317a3f6e..f9c9f3a2e 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/CharactersValidationBehavior_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/CharactersValidationBehavior_Tests.cs @@ -1,57 +1,63 @@ -using Xamarin.CommunityToolkit.Behaviors; +using System.Threading.Tasks; +using Xamarin.CommunityToolkit.Behaviors; using Xamarin.CommunityToolkit.UnitTests.Mocks; using Xamarin.Forms; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Behaviors { public class CharactersValidationBehavior_Tests { - public CharactersValidationBehavior_Tests() - => Device.PlatformServices = new MockPlatformServices(); + [SetUp] + public void Setup() => Device.PlatformServices = new MockPlatformServices(); - [Theory] - [InlineData(CharacterType.Any, 1, 2, "A", true)] - [InlineData(CharacterType.Any, 0, int.MaxValue, "", true)] - [InlineData(CharacterType.LowercaseLetter, 1, int.MaxValue, "WWWWWaWWWW", true)] - [InlineData(CharacterType.UppercaseLetter, 1, int.MaxValue, "aaaaaaRRaaaa", true)] - [InlineData(CharacterType.Letter, 4, int.MaxValue, "aaaaaaRRaaaa", true)] - [InlineData(CharacterType.Digit, 1, int.MaxValue, "-1d", true)] - [InlineData(CharacterType.Alphanumeric, 2, int.MaxValue, "@-3r", true)] - [InlineData(CharacterType.NonAlphanumericSymbol, 10, int.MaxValue, "@-&^%!+()/", true)] - [InlineData(CharacterType.LowercaseLatinLetter, 2, int.MaxValue, "HHHH a r.", true)] - [InlineData(CharacterType.UppercaseLatinLetter, 2, int.MaxValue, "aaaaaa....R.R.R.aaaa", true)] - [InlineData(CharacterType.LatinLetter, 5, int.MaxValue, "12345bBbBb", true)] - [InlineData(CharacterType.Whitespace, 0, int.MaxValue, ";lkjhgfd@+fasf", true)] - [InlineData(CharacterType.Any, 2, 2, "A", false)] - [InlineData(CharacterType.Any, 2, 2, "AaA", false)] - [InlineData(CharacterType.Any, 1, int.MaxValue, "", false)] - [InlineData(CharacterType.Any, 1, int.MaxValue, null, false)] - [InlineData(CharacterType.LowercaseLetter, 1, int.MaxValue, "WWWWWW", false)] - [InlineData(CharacterType.UppercaseLetter, 1, int.MaxValue, "aaaaaa", false)] - [InlineData(CharacterType.Letter, 4, int.MaxValue, "wHo", false)] - [InlineData(CharacterType.Digit, 1, int.MaxValue, "-d", false)] - [InlineData(CharacterType.Alphanumeric, 2, int.MaxValue, "@-3", false)] - [InlineData(CharacterType.NonAlphanumericSymbol, 1, int.MaxValue, "WWWWWWWW", false)] - [InlineData(CharacterType.LowercaseLatinLetter, 1, int.MaxValue, "Кириллица", false)] - [InlineData(CharacterType.UppercaseLatinLetter, 1, int.MaxValue, "КИРИЛЛИЦА", false)] - [InlineData(CharacterType.LatinLetter, 1, int.MaxValue, "Это Кириллица!", false)] - [InlineData(CharacterType.Whitespace, 0, 0, "WWWWWW WWWWW", false)] - public void IsValid(CharacterType characterType, int minimumCharactersNumber, int maximumCharactersNumber, string value, bool expectedValue) + [TestCase(CharacterType.Any, 1, 2, "A", true)] + [TestCase(CharacterType.Any, 0, int.MaxValue, "", true)] + [TestCase(CharacterType.LowercaseLetter, 1, int.MaxValue, "WWWWWaWWWW", true)] + [TestCase(CharacterType.UppercaseLetter, 1, int.MaxValue, "aaaaaaRRaaaa", true)] + [TestCase(CharacterType.Letter, 4, int.MaxValue, "aaaaaaRRaaaa", true)] + [TestCase(CharacterType.Digit, 1, int.MaxValue, "-1d", true)] + [TestCase(CharacterType.Alphanumeric, 2, int.MaxValue, "@-3r", true)] + [TestCase(CharacterType.NonAlphanumericSymbol, 10, int.MaxValue, "@-&^%!+()/", true)] + [TestCase(CharacterType.LowercaseLatinLetter, 2, int.MaxValue, "HHHH a r.", true)] + [TestCase(CharacterType.UppercaseLatinLetter, 2, int.MaxValue, "aaaaaa....R.R.R.aaaa", true)] + [TestCase(CharacterType.LatinLetter, 5, int.MaxValue, "12345bBbBb", true)] + [TestCase(CharacterType.Whitespace, 0, int.MaxValue, ";lkjhgfd@+fasf", true)] + [TestCase(CharacterType.Any, 2, 2, "A", false)] + [TestCase(CharacterType.Any, 2, 2, "AaA", false)] + [TestCase(CharacterType.Any, 1, int.MaxValue, "", false)] + [TestCase(CharacterType.Any, 1, int.MaxValue, null, false)] + [TestCase(CharacterType.LowercaseLetter, 1, int.MaxValue, "WWWWWW", false)] + [TestCase(CharacterType.UppercaseLetter, 1, int.MaxValue, "aaaaaa", false)] + [TestCase(CharacterType.Letter, 4, int.MaxValue, "wHo", false)] + [TestCase(CharacterType.Digit, 1, int.MaxValue, "-d", false)] + [TestCase(CharacterType.Alphanumeric, 2, int.MaxValue, "@-3", false)] + [TestCase(CharacterType.NonAlphanumericSymbol, 1, int.MaxValue, "WWWWWWWW", false)] + [TestCase(CharacterType.LowercaseLatinLetter, 1, int.MaxValue, "Кириллица", false)] + [TestCase(CharacterType.UppercaseLatinLetter, 1, int.MaxValue, "КИРИЛЛИЦА", false)] + [TestCase(CharacterType.LatinLetter, 1, int.MaxValue, "Это Кириллица!", false)] + [TestCase(CharacterType.Whitespace, 0, 0, "WWWWWW WWWWW", false)] + public async Task IsValid(CharacterType characterType, int minimumCharactersNumber, int maximumCharactersNumber, string value, bool expectedValue) { + // Arrange var behavior = new CharactersValidationBehavior { CharacterType = characterType, MinimumCharacterCount = minimumCharactersNumber, MaximumCharacterCount = maximumCharactersNumber }; + var entry = new Entry { Text = value }; entry.Behaviors.Add(behavior); - behavior.ForceValidate(); - Assert.Equal(expectedValue, behavior.IsValid); + + // Act + await behavior.ForceValidate(); + + // Assert + Assert.AreEqual(expectedValue, behavior.IsValid); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/EventToCommandBehaviorGeneric_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/EventToCommandBehaviorGeneric_Tests.cs new file mode 100644 index 000000000..8e3df334f --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/EventToCommandBehaviorGeneric_Tests.cs @@ -0,0 +1,170 @@ +using System; +using System.Reflection; +using Xamarin.CommunityToolkit.Behaviors; +using Xamarin.CommunityToolkit.Converters; +using Xamarin.CommunityToolkit.UnitTests.Mocks; +using Xamarin.Forms; +using NUnit.Framework; + +namespace Xamarin.CommunityToolkit.UnitTests.Behaviors +{ + public class EventToCommandBehaviorGeneric_Tests + { + [SetUp] + public void Setup() => Device.PlatformServices = new MockPlatformServices(); + + [Test] + public void ArgumentExceptionIfSpecifiedEventDoesNotExist() + { + var listView = new ListView(); + var behavior = new EventToCommandBehavior + { + EventName = "Wrong Event Name" + }; + Assert.Throws(() => listView.Behaviors.Add(behavior)); + } + + [Test] + public void NoExceptionIfSpecifiedEventExists() + { + var listView = new ListView(); + var behavior = new EventToCommandBehavior + { + EventName = nameof(ListView.ItemTapped) + }; + listView.Behaviors.Add(behavior); + } + + [Test] + public void NoExceptionIfAttachedToPage() + { + var page = new ContentPage(); + var behavior = new EventToCommandBehavior + { + EventName = nameof(Page.Appearing) + }; + page.Behaviors.Add(behavior); + } + + [Test] + public void NoExceptionWhenTheEventArgsAreNotNull() + { + var vm = new ViewModelCoffe(); + var behavior = new EventToCommandBehavior + { + EventName = nameof(ListView.ItemTapped), + EventArgsConverter = new ItemSelectedEventArgsConverter(), + Command = vm.SelectedCommand + }; + + Assert.Null(vm.CoffeeName); + var coffe = new Coffee { Id = 1, Name = "Café" }; + var eventArgs = new SelectedItemChangedEventArgs(coffe, 1); + + var notNullArgs = new object?[] { null, eventArgs }; + + TriggerEventToCommandBehavior(behavior, notNullArgs); + + Assert.AreEqual(coffe.Name, vm.CoffeeName); + } + + [Test] + public void NoExceptionWhenTheEventArgsAreNotNull_InheritedType() + { + var vm = new ViewModelCoffe(); + var behavior = new EventToCommandBehavior + { + EventName = nameof(ListView.ItemTapped), + EventArgsConverter = new ItemSelectedEventArgsConverter(), + Command = vm.SelectedCommand + }; + + Assert.Null(vm.CoffeeName); + var coffe = new Starbucks { Id = 1, Name = "Latte" }; + var eventArgs = new SelectedItemChangedEventArgs(coffe, 1); + + var notNullArgs = new object?[] { null, eventArgs }; + + TriggerEventToCommandBehavior(behavior, notNullArgs); + + Assert.AreEqual(coffe.Name, vm.CoffeeName); + } + + [Test] + public void ParameterOfTypeInt() + { + var vm = new ViewModelCoffe(); + var behavior = new EventToCommandBehavior + { + EventName = nameof(ListView.ItemTapped), + Command = vm.SelectedCommand, + CommandParameter = 2 + }; + + var nullArgs = new object?[] { null, null }; + + TriggerEventToCommandBehavior(behavior, nullArgs); + } + + [Test] + public void NoExceptionWhenTheSelectedItemIsNull() + { + var vm = new ViewModelCoffe(); + var behavior = new EventToCommandBehavior + { + EventName = nameof(ListView.ItemTapped), + EventArgsConverter = new ItemSelectedEventArgsConverter(), + Command = vm.SelectedCommand + }; + + Assert.Null(vm.CoffeeName); + var coffeNull = default(Coffee); + var notNullArgs = new object?[] { null, new SelectedItemChangedEventArgs(coffeNull, -1) }; + + TriggerEventToCommandBehavior(behavior, notNullArgs); + + Assert.Null(vm.CoffeeName); + } + + static void TriggerEventToCommandBehavior(EventToCommandBehavior eventToCommand, object?[] args) + { + var method = eventToCommand.GetType().GetMethod("OnTriggerHandled", BindingFlags.Instance | BindingFlags.NonPublic); + method?.Invoke(eventToCommand, args); + } + + class Starbucks : Coffee + { + } + + class Coffee + { + public int Id { get; set; } + + public string Roaster { get; set; } = string.Empty; + + public string? Name { get; set; } + + public string Image { get; set; } = string.Empty; + } + + class ViewModelCoffe + { + public Command SelectedCommand { get; set; } + + public string? CoffeeName { get; set; } + + public ViewModelCoffe() + { + SelectedCommand = new Command(Selected); + } + + void Selected(Coffee coffee) + { + if (coffee == null) + return; + + CoffeeName = coffee?.Name; + } + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/EventToCommandBehavior_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/EventToCommandBehavior_Tests.cs index 91787df9b..6806ed5a1 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/EventToCommandBehavior_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/EventToCommandBehavior_Tests.cs @@ -2,16 +2,16 @@ using Xamarin.CommunityToolkit.Behaviors; using Xamarin.CommunityToolkit.UnitTests.Mocks; using Xamarin.Forms; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Behaviors { public class EventToCommandBehavior_Tests { - public EventToCommandBehavior_Tests() - => Device.PlatformServices = new MockPlatformServices(); + [SetUp] + public void SetUp() => Device.PlatformServices = new MockPlatformServices(); - [Fact] + [Test] public void ArgumentExceptionIfSpecifiedEventDoesNotExist() { var listView = new ListView(); @@ -22,7 +22,7 @@ public void ArgumentExceptionIfSpecifiedEventDoesNotExist() Assert.Throws(() => listView.Behaviors.Add(behavior)); } - [Fact] + [Test] public void NoExceptionIfSpecifiedEventExists() { var listView = new ListView(); @@ -33,7 +33,7 @@ public void NoExceptionIfSpecifiedEventExists() listView.Behaviors.Add(behavior); } - [Fact] + [Test] public void NoExceptionIfAttachedToPage() { var page = new ContentPage(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/ImpliedOrderGridBehavior_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/ImpliedOrderGridBehavior_Tests.cs index e02fb2e88..91ee07778 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/ImpliedOrderGridBehavior_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/ImpliedOrderGridBehavior_Tests.cs @@ -1,13 +1,13 @@ using System; using Xamarin.CommunityToolkit.Behaviors; using Xamarin.Forms; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Behaviors { public class ImpliedOrderGridBehavior_Tests { - [Fact] + [Test] public void CorrectRowColumnAssignment() { var grid = new Grid(); @@ -73,7 +73,7 @@ public void CorrectRowColumnAssignment() AssertExpectedCoordinates(grid, new Label(), 4, 0); } - [Fact] + [Test] public void ThrowsOnManualAssignmentToUsedCell() { var grid = CreateExceptionTestGrid(); @@ -89,7 +89,7 @@ public void ThrowsOnManualAssignmentToUsedCell() Assert.Throws(() => grid.Children.Add(throwLabel)); } - [Fact] + [Test] public void ThrowsOnCellsExceeded() { var grid = CreateExceptionTestGrid(); @@ -110,7 +110,7 @@ public void ThrowsOnCellsExceeded() Assert.Throws(() => grid.Children.Add(new Label())); } - [Fact] + [Test] public void ThrowsOnSpanExceedsColumns() { var grid = CreateExceptionTestGrid(); @@ -121,7 +121,7 @@ public void ThrowsOnSpanExceedsColumns() Assert.Throws(() => grid.Children.Add(throwLabel)); } - [Fact] + [Test] public void ThrowsOnSpanExceedsRows() { var grid = CreateExceptionTestGrid(); @@ -149,8 +149,8 @@ Grid CreateExceptionTestGrid() void AssertExpectedCoordinates(Grid grid, View view, int row, int column) { grid.Children.Add(view); - Assert.Equal(row, Grid.GetRow(view)); - Assert.Equal(column, Grid.GetColumn(view)); + Assert.AreEqual(row, Grid.GetRow(view)); + Assert.AreEqual(column, Grid.GetColumn(view)); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/MaxLengthReachedBehavior_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/MaxLengthReachedBehavior_Tests.cs index 0760c3011..b6d66d3ff 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/MaxLengthReachedBehavior_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/MaxLengthReachedBehavior_Tests.cs @@ -2,13 +2,13 @@ using System.Windows.Input; using Xamarin.CommunityToolkit.Behaviors; using Xamarin.Forms; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Behaviors { public class MaxLengthReachedBehavior_Tests { - [Fact] + [Test] public void ShouldExecuteCommandWhenMaxLengthHasBeenReached() { // arrange @@ -20,10 +20,10 @@ public void ShouldExecuteCommandWhenMaxLengthHasBeenReached() entry.Text += "2"; // assert - Assert.True(commandHasBeenExecuted); + Assert.IsTrue(commandHasBeenExecuted); } - [Fact] + [Test] public void ShouldInvokeEventHandlerWhenMaxLengthHasBeenReached() { // arrange @@ -35,10 +35,10 @@ public void ShouldInvokeEventHandlerWhenMaxLengthHasBeenReached() entry.Text += "2"; // assert - Assert.True(eventHandlerHasBeenInvoked); + Assert.IsTrue(eventHandlerHasBeenInvoked); } - [Fact] + [Test] public void ShouldExecuteCommandWithTextValueNoLargerThenMaxLength() { // arrange @@ -51,10 +51,10 @@ public void ShouldExecuteCommandWithTextValueNoLargerThenMaxLength() entry.Text = "123456789"; // assert - Assert.Equal(expectedLength, actualLength); + Assert.AreEqual(expectedLength, actualLength); } - [Fact] + [Test] public void ShouldInvokeEventHandlerWithTextValueNoLargerThenMaxLength() { // arrange @@ -67,10 +67,10 @@ public void ShouldInvokeEventHandlerWithTextValueNoLargerThenMaxLength() entry.Text = "123456789"; // assert - Assert.Equal(expectedLength, actualLength); + Assert.AreEqual(expectedLength, actualLength); } - [Fact] + [Test] public void ShouldNotExecuteCommandBeforeMaxLengthHasBeenReached() { // arrange @@ -84,7 +84,7 @@ public void ShouldNotExecuteCommandBeforeMaxLengthHasBeenReached() Assert.False(commandHasBeenExecuted); } - [Fact] + [Test] public void ShouldNotInvokeEventHandlerBeforeMaxLengthHasBeenReached() { // arrange @@ -98,7 +98,7 @@ public void ShouldNotInvokeEventHandlerBeforeMaxLengthHasBeenReached() Assert.False(eventHandlerHasBeenInvoked); } - [Fact] + [Test] public void ShouldDismissKeyboardWhenMaxLengthHasBeenReached() { // arrange @@ -113,7 +113,7 @@ public void ShouldDismissKeyboardWhenMaxLengthHasBeenReached() Assert.False(entry.IsFocused); } - [Fact] + [Test] public void ShouldNotDismissKeyboardBeforeMaxLengthHasBeenReached() { // arrange @@ -124,10 +124,10 @@ public void ShouldNotDismissKeyboardBeforeMaxLengthHasBeenReached() entry.Text = "1"; // assert - Assert.True(entry.IsFocused); + Assert.IsTrue(entry.IsFocused); } - [Fact] + [Test] public void ShouldNotDismissKeyboardWhenOptionSetToFalse() { // arrange @@ -139,13 +139,13 @@ public void ShouldNotDismissKeyboardWhenOptionSetToFalse() entry.Text += "1"; // assert - Assert.True(entry.IsFocused); + Assert.IsTrue(entry.IsFocused); } Entry CreateEntry(int? maxLength = 2, bool shouldDismissKeyboardAutomatically = false, - ICommand command = null, - EventHandler eventHandler = null) + ICommand? command = null, + EventHandler? eventHandler = null) { var behavior = new MaxLengthReachedBehavior { diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/MultiValidationBehavior_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/MultiValidationBehavior_Tests.cs index 9318fd93b..bfb8a2648 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/MultiValidationBehavior_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/MultiValidationBehavior_Tests.cs @@ -1,59 +1,66 @@ -using Xamarin.CommunityToolkit.Behaviors; +using System.Threading.Tasks; +using Xamarin.CommunityToolkit.Behaviors; using Xamarin.CommunityToolkit.UnitTests.Mocks; using Xamarin.Forms; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Behaviors { public class MultiValidationBehavior_Tests { - public MultiValidationBehavior_Tests() - => Device.PlatformServices = new MockPlatformServices(); + [SetUp] + public void Setup() => Device.PlatformServices = new MockPlatformServices(); - [Theory] - [InlineData(CharacterType.Any, 1, 2, "A", true)] - [InlineData(CharacterType.Any, 0, int.MaxValue, "", true)] - [InlineData(CharacterType.LowercaseLetter, 1, int.MaxValue, "WWWWWaWWWW", true)] - [InlineData(CharacterType.UppercaseLetter, 1, int.MaxValue, "aaaaaaRRaaaa", true)] - [InlineData(CharacterType.Letter, 4, int.MaxValue, "aaaaaaRRaaaa", true)] - [InlineData(CharacterType.Digit, 1, int.MaxValue, "-1d", true)] - [InlineData(CharacterType.Alphanumeric, 2, int.MaxValue, "@-3r", true)] - [InlineData(CharacterType.NonAlphanumericSymbol, 10, int.MaxValue, "@-&^%!+()/", true)] - [InlineData(CharacterType.LowercaseLatinLetter, 2, int.MaxValue, "HHHH a r.", true)] - [InlineData(CharacterType.UppercaseLatinLetter, 2, int.MaxValue, "aaaaaa....R.R.R.aaaa", true)] - [InlineData(CharacterType.LatinLetter, 5, int.MaxValue, "12345bBbBb", true)] - [InlineData(CharacterType.Whitespace, 0, int.MaxValue, ";lkjhgfd@+fasf", true)] - [InlineData(CharacterType.Any, 2, 2, "A", false)] - [InlineData(CharacterType.Any, 2, 2, "AaA", false)] - [InlineData(CharacterType.Any, 1, int.MaxValue, "", false)] - [InlineData(CharacterType.Any, 1, int.MaxValue, null, false)] - [InlineData(CharacterType.LowercaseLetter, 1, int.MaxValue, "WWWWWW", false)] - [InlineData(CharacterType.UppercaseLetter, 1, int.MaxValue, "aaaaaa", false)] - [InlineData(CharacterType.Letter, 4, int.MaxValue, "wHo", false)] - [InlineData(CharacterType.Digit, 1, int.MaxValue, "-d", false)] - [InlineData(CharacterType.Alphanumeric, 2, int.MaxValue, "@-3", false)] - [InlineData(CharacterType.NonAlphanumericSymbol, 1, int.MaxValue, "WWWWWWWW", false)] - [InlineData(CharacterType.LowercaseLatinLetter, 1, int.MaxValue, "Кириллица", false)] - [InlineData(CharacterType.UppercaseLatinLetter, 1, int.MaxValue, "КИРИЛЛИЦА", false)] - [InlineData(CharacterType.LatinLetter, 1, int.MaxValue, "Это Кириллица!", false)] - [InlineData(CharacterType.Whitespace, 0, 0, "WWWWWW WWWWW", false)] - public void IsValid(CharacterType characterType, int minimumCharactersNumber, int maximumCharactersNumber, string value, bool expectedValue) + [TestCase(CharacterType.Any, 1, 2, "A", true)] + [TestCase(CharacterType.Any, 0, int.MaxValue, "", true)] + [TestCase(CharacterType.LowercaseLetter, 1, int.MaxValue, "WWWWWaWWWW", true)] + [TestCase(CharacterType.UppercaseLetter, 1, int.MaxValue, "aaaaaaRRaaaa", true)] + [TestCase(CharacterType.Letter, 4, int.MaxValue, "aaaaaaRRaaaa", true)] + [TestCase(CharacterType.Digit, 1, int.MaxValue, "-1d", true)] + [TestCase(CharacterType.Alphanumeric, 2, int.MaxValue, "@-3r", true)] + [TestCase(CharacterType.NonAlphanumericSymbol, 10, int.MaxValue, "@-&^%!+()/", true)] + [TestCase(CharacterType.LowercaseLatinLetter, 2, int.MaxValue, "HHHH a r.", true)] + [TestCase(CharacterType.UppercaseLatinLetter, 2, int.MaxValue, "aaaaaa....R.R.R.aaaa", true)] + [TestCase(CharacterType.LatinLetter, 5, int.MaxValue, "12345bBbBb", true)] + [TestCase(CharacterType.Whitespace, 0, int.MaxValue, ";lkjhgfd@+fasf", true)] + [TestCase(CharacterType.Any, 2, 2, "A", false)] + [TestCase(CharacterType.Any, 2, 2, "AaA", false)] + [TestCase(CharacterType.Any, 1, int.MaxValue, "", false)] + [TestCase(CharacterType.Any, 1, int.MaxValue, null, false)] + [TestCase(CharacterType.LowercaseLetter, 1, int.MaxValue, "WWWWWW", false)] + [TestCase(CharacterType.UppercaseLetter, 1, int.MaxValue, "aaaaaa", false)] + [TestCase(CharacterType.Letter, 4, int.MaxValue, "wHo", false)] + [TestCase(CharacterType.Digit, 1, int.MaxValue, "-d", false)] + [TestCase(CharacterType.Alphanumeric, 2, int.MaxValue, "@-3", false)] + [TestCase(CharacterType.NonAlphanumericSymbol, 1, int.MaxValue, "WWWWWWWW", false)] + [TestCase(CharacterType.LowercaseLatinLetter, 1, int.MaxValue, "Кириллица", false)] + [TestCase(CharacterType.UppercaseLatinLetter, 1, int.MaxValue, "КИРИЛЛИЦА", false)] + [TestCase(CharacterType.LatinLetter, 1, int.MaxValue, "Это Кириллица!", false)] + [TestCase(CharacterType.Whitespace, 0, 0, "WWWWWW WWWWW", false)] + public async Task IsValid(CharacterType characterType, int minimumCharactersNumber, int maximumCharactersNumber, string value, bool expectedValue) { - var multiBehavior = new MultiValidationBehavior(); + // Arrange var behavior = new CharactersValidationBehavior { CharacterType = characterType, MinimumCharacterCount = minimumCharactersNumber, MaximumCharacterCount = maximumCharactersNumber }; + + var multiBehavior = new MultiValidationBehavior(); multiBehavior.Children.Add(behavior); + var entry = new Entry { Text = value }; entry.Behaviors.Add(multiBehavior); - multiBehavior.ForceValidate(); - Assert.Equal(expectedValue, multiBehavior.IsValid); + + // Act + await multiBehavior.ForceValidate(); + + // Assert + Assert.AreEqual(expectedValue, multiBehavior.IsValid); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/NumericValidationBehavior_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/NumericValidationBehavior_Tests.cs index 5494be7c2..711eea8cb 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/NumericValidationBehavior_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/NumericValidationBehavior_Tests.cs @@ -1,70 +1,76 @@ using System.Globalization; +using System.Threading.Tasks; using Xamarin.CommunityToolkit.Behaviors; using Xamarin.CommunityToolkit.UnitTests.Mocks; using Xamarin.Forms; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Behaviors { public class NumericValidationBehavior_Tests { - public NumericValidationBehavior_Tests() - => Device.PlatformServices = new MockPlatformServices(); + [SetUp] + public void Setup() => Device.PlatformServices = new MockPlatformServices(); - [Theory] - [InlineData("en-US", "15.2", 1.0, 16.0, 0, 16, true)] - [InlineData("en-US", "15.", 1.0, 16.0, 0, 1, true)] - [InlineData("en-US", "15.88", 1.0, 16.0, 2, 2, true)] - [InlineData("en-US", "0.99", 0.9, 2.0, 0, 16, true)] - [InlineData("en-US", ".99", 0.9, 2.0, 0, 16, true)] - [InlineData("en-US", "1,115.2", 1.0, 2000.0, 0, 16, true)] - [InlineData("de-DE", "15,2", 1.0, 16.0, 0, 16, true)] - [InlineData("de-DE", "15,", 1.0, 16.0, 0, 1, true)] - [InlineData("de-DE", "15,88", 1.0, 16.0, 2, 2, true)] - [InlineData("de-DE", "0,99", 0.9, 2.0, 0, 16, true)] - [InlineData("de-DE", ",99", 0.9, 2.0, 0, 16, true)] - [InlineData("de-DE", "1.115,2", 1.0, 2000.0, 0, 16, true)] - [InlineData("en-US", "15.3", 16.0, 20.0, 0, 16, false)] - [InlineData("en-US", "15.3", 0.0, 15.0, 0, 16, false)] - [InlineData("en-US", "15.", 1.0, 16.0, 0, 0, false)] - [InlineData("en-US", ".7", 0.0, 16.0, 0, 0, false)] - [InlineData("en-US", "15", 1.0, 16.0, 1, 16, false)] - [InlineData("en-US", "", 0.0, 16.0, 0, 16, false)] - [InlineData("en-US", " ", 0.0, 16.0, 0, 16, false)] - [InlineData("en-US", null, 0.0, 16.0, 0, 16, false)] - [InlineData("en-US", "15,2", 1.0, 16.0, 0, 16, false)] - [InlineData("en-US", "1.115,2", 1.0, 2000.0, 0, 16, false)] - [InlineData("de-DE", "15,3", 16.0, 20.0, 0, 16, false)] - [InlineData("de-DE", "15,3", 0.0, 15.0, 0, 16, false)] - [InlineData("de-DE", "15,", 1.0, 16.0, 0, 0, false)] - [InlineData("de-DE", ",7", 0.0, 16.0, 0, 0, false)] - [InlineData("de-DE", "15", 1.0, 16.0, 1, 16, false)] - [InlineData("de-DE", "", 0.0, 16.0, 0, 16, false)] - [InlineData("de-DE", " ", 0.0, 16.0, 0, 16, false)] - [InlineData("de-DE", null, 0.0, 16.0, 0, 16, false)] - [InlineData("de-DE", "15.2", 1.0, 16.0, 0, 16, false)] - [InlineData("de-DE", "1,115.2", 1.0, 2000.0, 0, 16, false)] - public void IsValid(string culture, string value, double minValue, double maxValue, int minDecimalPlaces, int maxDecimalPlaces, bool expectedValue) + [TestCase("en-US", "15.2", 1.0, 16.0, 0, 16, true)] + [TestCase("en-US", "15.", 1.0, 16.0, 0, 1, true)] + [TestCase("en-US", "15.88", 1.0, 16.0, 2, 2, true)] + [TestCase("en-US", "0.99", 0.9, 2.0, 0, 16, true)] + [TestCase("en-US", ".99", 0.9, 2.0, 0, 16, true)] + [TestCase("en-US", "1,115.2", 1.0, 2000.0, 0, 16, true)] + [TestCase("de-DE", "15,2", 1.0, 16.0, 0, 16, true)] + [TestCase("de-DE", "15,", 1.0, 16.0, 0, 1, true)] + [TestCase("de-DE", "15,88", 1.0, 16.0, 2, 2, true)] + [TestCase("de-DE", "0,99", 0.9, 2.0, 0, 16, true)] + [TestCase("de-DE", ",99", 0.9, 2.0, 0, 16, true)] + [TestCase("de-DE", "1.115,2", 1.0, 2000.0, 0, 16, true)] + [TestCase("en-US", "15.3", 16.0, 20.0, 0, 16, false)] + [TestCase("en-US", "15.3", 0.0, 15.0, 0, 16, false)] + [TestCase("en-US", "15.", 1.0, 16.0, 0, 0, false)] + [TestCase("en-US", ".7", 0.0, 16.0, 0, 0, false)] + [TestCase("en-US", "15", 1.0, 16.0, 1, 16, false)] + [TestCase("en-US", "", 0.0, 16.0, 0, 16, false)] + [TestCase("en-US", " ", 0.0, 16.0, 0, 16, false)] + [TestCase("en-US", null, 0.0, 16.0, 0, 16, false)] + [TestCase("en-US", "15,2", 1.0, 16.0, 0, 16, false)] + [TestCase("en-US", "1.115,2", 1.0, 2000.0, 0, 16, false)] + [TestCase("de-DE", "15,3", 16.0, 20.0, 0, 16, false)] + [TestCase("de-DE", "15,3", 0.0, 15.0, 0, 16, false)] + [TestCase("de-DE", "15,", 1.0, 16.0, 0, 0, false)] + [TestCase("de-DE", ",7", 0.0, 16.0, 0, 0, false)] + [TestCase("de-DE", "15", 1.0, 16.0, 1, 16, false)] + [TestCase("de-DE", "", 0.0, 16.0, 0, 16, false)] + [TestCase("de-DE", " ", 0.0, 16.0, 0, 16, false)] + [TestCase("de-DE", null, 0.0, 16.0, 0, 16, false)] + [TestCase("de-DE", "15.2", 1.0, 16.0, 0, 16, false)] + [TestCase("de-DE", "1,115.2", 1.0, 2000.0, 0, 16, false)] + public async Task IsValid(string culture, string value, double minValue, double maxValue, int minDecimalPlaces, int maxDecimalPlaces, bool expectedValue) { + // Arrange var origCulture = CultureInfo.CurrentCulture; CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo(culture); + var behavior = new NumericValidationBehavior + { + MinimumValue = minValue, + MaximumValue = maxValue, + MinimumDecimalPlaces = minDecimalPlaces, + MaximumDecimalPlaces = maxDecimalPlaces + }; + + var entry = new Entry + { + Text = value + }; + entry.Behaviors.Add(behavior); + try { - var behavior = new NumericValidationBehavior - { - MinimumValue = minValue, - MaximumValue = maxValue, - MinimumDecimalPlaces = minDecimalPlaces, - MaximumDecimalPlaces = maxDecimalPlaces - }; - var entry = new Entry - { - Text = value - }; - entry.Behaviors.Add(behavior); - behavior.ForceValidate(); - Assert.Equal(expectedValue, behavior.IsValid); + // Act + await behavior.ForceValidate(); + + // Assert + Assert.AreEqual(expectedValue, behavior.IsValid); } finally { diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/RequiredStringValidationBehavior_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/RequiredStringValidationBehavior_Tests.cs index 1aa160def..e0bcd48f7 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/RequiredStringValidationBehavior_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/RequiredStringValidationBehavior_Tests.cs @@ -1,78 +1,87 @@ -using Xamarin.CommunityToolkit.Behaviors; +using Xamarin.CommunityToolkit.Behaviors; using Xamarin.CommunityToolkit.UnitTests.Mocks; using Xamarin.Forms; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Behaviors { public class RequiredStringValidationBehavior_Tests { - public RequiredStringValidationBehavior_Tests() - => Device.PlatformServices = new MockPlatformServices(); + [SetUp] + public void Setup() => Device.PlatformServices = new MockPlatformServices(); - [Fact] + [Test] public void IsValidTrueWhenBothIsNull_Test() { - //arrange + // Arrange var passwordEntry = new Entry(); var confirmPasswordEntry = new Entry(); var confirmPasswordBehavior = new RequiredStringValidationBehavior(); confirmPasswordBehavior.Flags = ValidationFlags.ValidateOnAttaching; - //act + + // Act confirmPasswordBehavior.RequiredString = passwordEntry.Text; confirmPasswordEntry.Behaviors.Add(confirmPasswordBehavior); - //assert - Assert.True(confirmPasswordBehavior.IsValid); + + // Assert + Assert.IsTrue(confirmPasswordBehavior.IsValid); } - [Fact] + [Test] public void IsValidFalseWhenOneIsNull_Test() { - //arrange + // Arrange var passwordEntry = new Entry(); var confirmPasswordEntry = new Entry(); var confirmPasswordBehavior = new RequiredStringValidationBehavior(); confirmPasswordBehavior.Flags = ValidationFlags.ValidateOnAttaching; - //act + + // Act passwordEntry.Text = "123456"; confirmPasswordBehavior.RequiredString = passwordEntry.Text; confirmPasswordEntry.Behaviors.Add(confirmPasswordBehavior); + confirmPasswordEntry.Text = null; - //assert + + // Assert Assert.False(confirmPasswordBehavior.IsValid); } - [Fact] + [Test] public void IsValidTrueWhenEnterSameText_Test() { - //arrange + // Arrange var passwordEntry = new Entry(); var confirmPasswordEntry = new Entry(); var confirmPasswordBehavior = new RequiredStringValidationBehavior(); confirmPasswordBehavior.Flags = ValidationFlags.ValidateOnValueChanging; - //act + + // Act passwordEntry.Text = "123456"; confirmPasswordBehavior.RequiredString = passwordEntry.Text; confirmPasswordEntry.Behaviors.Add(confirmPasswordBehavior); confirmPasswordEntry.Text = "123456"; - //assert - Assert.True(confirmPasswordBehavior.IsValid); + + // Assert + Assert.IsTrue(confirmPasswordBehavior.IsValid); } - [Fact] + [Test] public void IsValidFalseWhenEnterDifferentText_Test() { - //arrange + // Arrange var passwordEntry = new Entry(); var confirmPasswordEntry = new Entry(); var confirmPasswordBehavior = new RequiredStringValidationBehavior(); confirmPasswordBehavior.Flags = ValidationFlags.ValidateOnValueChanging; - //act + + // Act passwordEntry.Text = "123456"; confirmPasswordBehavior.RequiredString = passwordEntry.Text; confirmPasswordEntry.Behaviors.Add(confirmPasswordBehavior); confirmPasswordEntry.Text = "1234567"; - //assert + + // Assert Assert.False(confirmPasswordBehavior.IsValid); } } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/SetFocusOnEntryCompleted_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/SetFocusOnEntryCompleted_Tests.cs new file mode 100644 index 000000000..84a9b7380 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/SetFocusOnEntryCompleted_Tests.cs @@ -0,0 +1,54 @@ +using Xamarin.CommunityToolkit.Behaviors; +using Xamarin.Forms; +using NUnit.Framework; + +namespace Xamarin.CommunityToolkit.UnitTests.Behaviors +{ + public class SetFocusOnEntryCompleted_Tests + { + [Test] + public void DoesNotSetFocusBeforeCompletion() + { + // arrange + var entry2 = CreateEntry(); + var entry1 = CreateEntry(entry2); + + // act + entry1.Focus(); + entry1.Text = "text"; + + // assert + Assert.False(entry2.IsFocused); + } + + [Test] + public void SetsFocusWhenCompleted() + { + // arrange + var entry2 = CreateEntry(); + var entry1 = CreateEntry(entry2); + + // act + entry1.Focus(); + entry1.SendCompleted(); + + // assert + Assert.IsTrue(entry2.IsFocused); + } + + public Entry CreateEntry(VisualElement? nextElement = null) + { + var entry = new Entry(); + + // We simulate Focus/Unfocus behavior ourselves + // because unit tests doesn't have "platform-specific" part + // where IsFocused is controlled in the real app + entry.FocusChangeRequested += (s, e) => entry.SetValue(VisualElement.IsFocusedPropertyKey, e.Focus); + + if (nextElement != null) + SetFocusOnEntryCompletedBehavior.SetNextElement(entry, nextElement); + + return entry; + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/UriValidationBehavior_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/UriValidationBehavior_Tests.cs index b9e9406b5..647b8688f 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/UriValidationBehavior_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/UriValidationBehavior_Tests.cs @@ -1,29 +1,31 @@ using System; +using System.Threading.Tasks; using Xamarin.CommunityToolkit.Behaviors; using Xamarin.CommunityToolkit.UnitTests.Mocks; using Xamarin.Forms; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Behaviors { public class UriValidationBehavior_Tests { - public UriValidationBehavior_Tests() - => Device.PlatformServices = new MockPlatformServices(); + [SetUp] + public void Setup() => Device.PlatformServices = new MockPlatformServices(); - [Theory] - [InlineData(@"http://microsoft.com", UriKind.Absolute, true)] - [InlineData(@"microsoft/xamarin/news", UriKind.Relative, true)] - [InlineData(@"http://microsoft.com", UriKind.RelativeOrAbsolute, true)] - [InlineData(@"microsoftcom", UriKind.Absolute, false)] - [InlineData(@"microsoft\\\\\xamarin/news", UriKind.Relative, false)] - [InlineData(@"ht\\\.com", UriKind.RelativeOrAbsolute, false)] - public void IsValid(string value, UriKind uriKind, bool expectedValue) + [TestCase(@"http://microsoft.com", UriKind.Absolute, true)] + [TestCase(@"microsoft/xamarin/news", UriKind.Relative, true)] + [TestCase(@"http://microsoft.com", UriKind.RelativeOrAbsolute, true)] + [TestCase(@"microsoftcom", UriKind.Absolute, false)] + [TestCase(@"microsoft\\\\\xamarin/news", UriKind.Relative, false)] + [TestCase(@"ht\\\.com", UriKind.RelativeOrAbsolute, false)] + public async Task IsValid(string value, UriKind uriKind, bool expectedValue) { + // Arrange var behavior = new UriValidationBehavior { UriKind = uriKind, }; + var entry = new Entry { Text = value, @@ -33,8 +35,12 @@ public void IsValid(string value, UriKind uriKind, bool expectedValue) } }; entry.Behaviors.Add(behavior); - behavior.ForceValidate(); - Assert.Equal(expectedValue, behavior.IsValid); + + // Act + await behavior.ForceValidate(); + + // Assert + Assert.AreEqual(expectedValue, behavior.IsValid); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/UserStoppedTypingBehavior_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/UserStoppedTypingBehavior_Tests.cs index b69fdc3eb..b7d91f471 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/UserStoppedTypingBehavior_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Behaviors/UserStoppedTypingBehavior_Tests.cs @@ -2,7 +2,7 @@ using System.Windows.Input; using Xamarin.CommunityToolkit.Behaviors; using Xamarin.Forms; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Behaviors { @@ -10,44 +10,45 @@ public class UserStoppedTypingBehavior_Tests { const int defaultTimeThreshold = 1000; const int defaultLengthThreshold = 0; + const int defaultTimeoutThreshold = defaultTimeThreshold * 2; - [Fact] + [Test] public async Task ShouldExecuteCommandWhenTimeThresholdHasExpired() { // arrange var commandHasBeenExecuted = false; - var entry = CreateEntryWithBehavior(command: new Command((s) => commandHasBeenExecuted = true)); + var entry = CreateEntryWithBehavior(command: new Command(_ => commandHasBeenExecuted = true)); // act entry.Text = "1"; - await Task.Delay(defaultTimeThreshold + 100); + await Task.Delay(defaultTimeoutThreshold); // assert - Assert.True(commandHasBeenExecuted); + Assert.IsTrue(commandHasBeenExecuted); } - [Fact] + [Test] public async Task ShouldExecuteCommandWithSpecificParameterWhenSpecified() { // arrange var commandHasBeenExecuted = false; - var entry = CreateEntryWithBehavior(command: new Command((s) => commandHasBeenExecuted = true), + var entry = CreateEntryWithBehavior(command: new Command(_ => commandHasBeenExecuted = true), commandParameter: true); // act entry.Text = "1"; - await Task.Delay(defaultTimeThreshold + 100); + await Task.Delay(defaultTimeoutThreshold); // assert - Assert.True(commandHasBeenExecuted); + Assert.IsTrue(commandHasBeenExecuted); } - [Fact] + [Test] public async Task ShouldNotExecuteCommandBeforeTimeThresholdHasExpired() { // arrange var commandHasBeenExecuted = false; - var entry = CreateEntryWithBehavior(command: new Command((s) => commandHasBeenExecuted = true)); + var entry = CreateEntryWithBehavior(command: new Command(_ => commandHasBeenExecuted = true)); // act entry.Text = "1"; @@ -57,25 +58,25 @@ public async Task ShouldNotExecuteCommandBeforeTimeThresholdHasExpired() Assert.False(commandHasBeenExecuted); } - [Fact] + [Test] public async Task ShouldOnlyExectueCommandOnceWhenTextChangedHasOccurredMultipleTimes() { // arrange var timesExecuted = 0; - var entry = CreateEntryWithBehavior(command: new Command((s) => timesExecuted++)); + var entry = CreateEntryWithBehavior(command: new Command(_ => timesExecuted++)); // act entry.Text = "1"; entry.Text = "12"; entry.Text = "123"; entry.Text = "1234"; - await Task.Delay(defaultTimeThreshold + 100); + await Task.Delay(defaultTimeoutThreshold); // assert - Assert.Equal(1, timesExecuted); + Assert.AreEqual(1, timesExecuted); } - [Fact] + [Test] public async Task ShouldDismissKeyboardWhenTimeThresholdHasExpired() { // arrange @@ -85,47 +86,47 @@ public async Task ShouldDismissKeyboardWhenTimeThresholdHasExpired() entry.Focus(); entry.Text = "1"; - await Task.Delay(defaultTimeThreshold + 100); + await Task.Delay(defaultTimeoutThreshold); // assert Assert.False(entry.IsFocused); } - [Fact] + [Test] public async Task ShouldExecuteCommandWhenMinimumLengthThreholdHasBeenReached() { // arrange var commandHasBeenExecuted = false; - var entry = CreateEntryWithBehavior(command: new Command((s) => commandHasBeenExecuted = true), + var entry = CreateEntryWithBehavior(command: new Command(_ => commandHasBeenExecuted = true), lengthThreshold: 3); // act entry.Text = "1"; entry.Text = "12"; entry.Text = "123"; - await Task.Delay(defaultTimeThreshold + 100); + await Task.Delay(defaultTimeoutThreshold); // assert - Assert.True(commandHasBeenExecuted); + Assert.IsTrue(commandHasBeenExecuted); } - [Fact] + [Test] public async Task ShouldNotExecuteCommandWhenMinimumLengthThreholdHasNotBeenReached() { // arrange var commandHasBeenExecuted = false; - var entry = CreateEntryWithBehavior(command: new Command((s) => commandHasBeenExecuted = true), + var entry = CreateEntryWithBehavior(command: new Command(_ => commandHasBeenExecuted = true), lengthThreshold: 2); // act entry.Text = "1"; - await Task.Delay(defaultTimeThreshold + 100); + await Task.Delay(defaultTimeoutThreshold); // assert Assert.False(commandHasBeenExecuted); } - [Fact] + [Test] public async Task ShouldNotDismissKeyboardWhenMinimumLengthThreholdHasNotBeenReached() { // arrange @@ -136,32 +137,32 @@ public async Task ShouldNotDismissKeyboardWhenMinimumLengthThreholdHasNotBeenRea entry.Focus(); entry.Text = "1"; - await Task.Delay(defaultTimeThreshold + 100); + await Task.Delay(defaultTimeoutThreshold); // assert - Assert.True(entry.IsFocused); + Assert.IsTrue(entry.IsFocused); } - [Fact] + [Test] public async Task ShouldExecuteCommandImmediatelyWhenMinimumLengthThreholdHasNotBeenSet() { // arrange var commandHasBeenExecuted = false; - var entry = CreateEntryWithBehavior(command: new Command((s) => commandHasBeenExecuted = true)); + var entry = CreateEntryWithBehavior(command: new Command(_ => commandHasBeenExecuted = true)); // act entry.Text = "1"; - await Task.Delay(defaultTimeThreshold + 100); + await Task.Delay(defaultTimeoutThreshold); // assert - Assert.True(commandHasBeenExecuted); + Assert.IsTrue(commandHasBeenExecuted); } public Entry CreateEntryWithBehavior(int timeThreshold = defaultTimeThreshold, int lengthThreshold = defaultLengthThreshold, bool shouldDismissKeyboardAutomatically = false, - ICommand command = null, - object commandParameter = null) + ICommand? command = null, + object? commandParameter = null) { var entry = new Entry { diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/BoolToObjectConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/BoolToObjectConverter_Tests.cs index 29d952be8..5a3c224e2 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/BoolToObjectConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/BoolToObjectConverter_Tests.cs @@ -1,7 +1,7 @@ using System; using System.Globalization; using Xamarin.CommunityToolkit.Converters; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { @@ -10,9 +10,8 @@ public class BoolToObjectConverter_Tests public const string TrueTestObject = nameof(TrueTestObject); public const string FalseTestObject = nameof(FalseTestObject); - [Theory] - [InlineData(true, TrueTestObject, FalseTestObject, TrueTestObject)] - [InlineData(false, TrueTestObject, FalseTestObject, FalseTestObject)] + [TestCase(true, TrueTestObject, FalseTestObject, TrueTestObject)] + [TestCase(false, TrueTestObject, FalseTestObject, FalseTestObject)] public void BoolToObjectConvert(bool value, object trueObject, object falseObject, object expectedResult) { var boolObjectConverter = new BoolToObjectConverter(); @@ -21,12 +20,11 @@ public void BoolToObjectConvert(bool value, object trueObject, object falseObjec var result = boolObjectConverter.Convert(value, typeof(BoolToObjectConverter_Tests), null, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } - [Theory] - [InlineData(TrueTestObject, TrueTestObject, FalseTestObject, true)] - [InlineData(FalseTestObject, TrueTestObject, FalseTestObject, false)] + [TestCase(TrueTestObject, TrueTestObject, FalseTestObject, true)] + [TestCase(FalseTestObject, TrueTestObject, FalseTestObject, false)] public void BoolToObjectConvertBack(object value, object trueObject, object falseObject, bool expectedResult) { var boolObjectConverter = new BoolToObjectConverter(); @@ -35,11 +33,10 @@ public void BoolToObjectConvertBack(object value, object trueObject, object fals var result = boolObjectConverter.ConvertBack(value, typeof(BoolToObjectConverter_Tests), null, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } - [Theory] - [InlineData("")] + [TestCase("")] public void BoolToObjectInValidValuesThrowArgumenException(object value) { var boolObjectConverter = new BoolToObjectConverter(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ByteArrayToImageSourceConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ByteArrayToImageSourceConverter_Tests.cs index a30e013ee..e2b3a54d8 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ByteArrayToImageSourceConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ByteArrayToImageSourceConverter_Tests.cs @@ -3,13 +3,13 @@ using System.IO; using Xamarin.CommunityToolkit.Converters; using Xamarin.Forms; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { public class ByteArrayToImageSourceConverter_Tests { - [Fact] + [Test] public void ByteArrayToImageSourceConverter() { var byteArray = new byte[] { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }; @@ -22,11 +22,11 @@ public void ByteArrayToImageSourceConverter() var result = byteArrayToImageSourceConverter.Convert(byteArray, typeof(ByteArrayToImageSourceConverter), null, CultureInfo.CurrentCulture); - Assert.True(StreamEquals(GetStreamFromImageSource((ImageSource)result), memoryStream)); + Assert.IsTrue(StreamEquals(GetStreamFromImageSource((ImageSource?)result), memoryStream)); } - [Theory] - [InlineData("Random String Value")] + + [TestCase("Random String Value")] public void InvalidConverterValuesReturnsNull(object value) { var byteArrayToImageSourceConverter = new ByteArrayToImageSourceConverter(); @@ -34,28 +34,32 @@ public void InvalidConverterValuesReturnsNull(object value) Assert.Throws(() => byteArrayToImageSourceConverter.Convert(value, typeof(ByteArrayToImageSourceConverter), null, CultureInfo.CurrentCulture)); } - Stream GetStreamFromImageSource(ImageSource imageSource) + Stream? GetStreamFromImageSource(ImageSource? imageSource) { - var streamImageSource = (StreamImageSource)imageSource; + var streamImageSource = (StreamImageSource?)imageSource; var cancellationToken = System.Threading.CancellationToken.None; - var task = streamImageSource.Stream(cancellationToken); - return task.Result; + var task = streamImageSource?.Stream(cancellationToken); + return task?.Result; } - bool StreamEquals(Stream a, Stream b) + bool StreamEquals(Stream? a, Stream? b) { if (a == b) return true; - if (a == null || - b == null || - a.Length != b.Length) + if (a == null + || b == null + || a.Length != b.Length) + { return false; + } for (var i = 0; i < a.Length; i++) + { if (a.ReadByte() != b.ReadByte()) return false; + } return true; } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/DateTimeOffsetConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/DateTimeOffsetConverter_Tests.cs index 27df80bd6..7a9ef254b 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/DateTimeOffsetConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/DateTimeOffsetConverter_Tests.cs @@ -2,24 +2,25 @@ using System.Collections.Generic; using System.Globalization; using Xamarin.CommunityToolkit.Converters; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { public class DateTimeOffsetConverter_Tests { - static DateTime testDateTimeNow = DateTime.Now; - static DateTime testDateTimeLocal = new DateTime(2020, 08, 25, 13, 37, 00, DateTimeKind.Local); - static DateTime testDateTimeUtc = new DateTime(2020, 08, 25, 13, 37, 00, DateTimeKind.Utc); - static DateTime testDateTimeUnspecified = new DateTime(2020, 08, 25, 13, 37, 00); - static DateTimeOffset testDateTimeOffsetNow = new DateTimeOffset(testDateTimeNow); - static DateTimeOffset testDateTimeOffsetLocal = new DateTimeOffset(2020, 08, 25, 13, 37, 00, DateTimeOffset.Now.Offset); - static DateTimeOffset testDateTimeOffsetUtc = new DateTimeOffset(2020, 08, 25, 13, 37, 00, DateTimeOffset.UtcNow.Offset); + static readonly DateTime testDateTimeNow = DateTime.Now; + static readonly DateTime testDateTimeLocal = new DateTime(2020, 08, 25, 13, 37, 00, DateTimeKind.Local); + static readonly DateTime testDateTimeUtc = new DateTime(2020, 08, 25, 13, 37, 00, DateTimeKind.Utc); + static readonly DateTime testDateTimeUnspecified = new DateTime(2020, 08, 25, 13, 37, 00); + + static readonly DateTimeOffset testDateTimeOffsetNow = new DateTimeOffset(testDateTimeNow); + static readonly DateTimeOffset testDateTimeOffsetLocal = new DateTimeOffset(2020, 08, 25, 13, 37, 00, DateTimeOffset.Now.Offset); + static readonly DateTimeOffset testDateTimeOffsetUtc = new DateTimeOffset(2020, 08, 25, 13, 37, 00, DateTimeOffset.UtcNow.Offset); public static IEnumerable GetData() => new List { - new object[] { testDateTimeNow, testDateTimeNow }, + new object[] { testDateTimeOffsetNow, testDateTimeNow }, new object[] { DateTimeOffset.MinValue, DateTime.MinValue }, new object[] { DateTimeOffset.MaxValue, DateTime.MaxValue }, new object[] { testDateTimeOffsetLocal, testDateTimeLocal }, @@ -30,7 +31,7 @@ public class DateTimeOffsetConverter_Tests public static IEnumerable GetDataReverse() => new List { - new object[] { testDateTimeNow, testDateTimeNow }, + new object[] { testDateTimeNow, testDateTimeOffsetNow }, new object[] { DateTime.MinValue, DateTimeOffset.MinValue }, new object[] { DateTime.MaxValue, DateTimeOffset.MaxValue }, new object[] { testDateTimeLocal, testDateTimeOffsetLocal }, @@ -38,8 +39,7 @@ public class DateTimeOffsetConverter_Tests new object[] { testDateTimeUnspecified, testDateTimeOffsetUtc }, }; - [Theory] - [MemberData(nameof(GetData))] + [TestCaseSource(nameof(GetData))] public void DateTimeOffsetConverter(DateTimeOffset value, DateTime expectedResult) { var dateTimeOffsetConverter = new DateTimeOffsetConverter(); @@ -47,11 +47,10 @@ public void DateTimeOffsetConverter(DateTimeOffset value, DateTime expectedResul var result = dateTimeOffsetConverter.Convert(value, typeof(DateTimeOffsetConverter_Tests), null, CultureInfo.CurrentCulture); - Assert.Equal(expectedResult, result); + Assert.AreEqual(expectedResult, result); } - [Theory] - [MemberData(nameof(GetDataReverse))] + [TestCaseSource(nameof(GetDataReverse))] public void DateTimeOffsetConverterBack(DateTime value, DateTimeOffset expectedResult) { var dateTimeOffsetConverter = new DateTimeOffsetConverter(); @@ -59,10 +58,10 @@ public void DateTimeOffsetConverterBack(DateTime value, DateTimeOffset expectedR var result = dateTimeOffsetConverter.ConvertBack(value, typeof(DateTimeOffsetConverter_Tests), null, CultureInfo.CurrentCulture); - Assert.Equal(expectedResult, result); + Assert.AreEqual(expectedResult, result); } - [Fact] + [Test] public void DateTimeOffsetConverter_GivenInvalidParameters_ThrowsException() { var dateTimeOffsetConverter = new DateTimeOffsetConverter(); @@ -72,7 +71,7 @@ public void DateTimeOffsetConverter_GivenInvalidParameters_ThrowsException() CultureInfo.CurrentCulture)); } - [Fact] + [Test] public void DateTimeOffsetConverterBack_GivenInvalidParameters_ThrowsException() { var dateTimeOffsetConverter = new DateTimeOffsetConverter(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/DoubleToIntConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/DoubleToIntConverter_Tests.cs index abd7560e3..f8811bfb8 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/DoubleToIntConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/DoubleToIntConverter_Tests.cs @@ -1,39 +1,36 @@ using System; using System.Globalization; using Xamarin.CommunityToolkit.Converters; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { public class DoubleToIntConverter_Tests { - [Theory] - [InlineData(2.5, 2)] - [InlineData(2.55, 3)] - [InlineData(2.555, 3)] - [InlineData(2.555, 652, 255)] - public void DoubleToIntConverter(double value, int expectedResult, object ratio = null) + [TestCase(2.5, 2)] + [TestCase(2.55, 3)] + [TestCase(2.555, 3)] + [TestCase(2.555, 652, 255)] + public void DoubleToIntConverter(double value, int expectedResult, object? ratio = null) { var doubleToIntConverter = new DoubleToIntConverter(); var result = doubleToIntConverter.Convert(value, typeof(DoubleToIntConverter_Tests), ratio, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } - [Theory] - [InlineData(2, 2)] - public void DoubleToIntConverterBack(int value, double expectedResult, object ratio = null) + [TestCase(2, 2)] + public void DoubleToIntConverterBack(int value, double expectedResult, object? ratio = null) { var doubleToIntConverter = new DoubleToIntConverter(); var result = doubleToIntConverter.ConvertBack(value, typeof(DoubleToIntConverter_Tests), ratio, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } - [Theory] - [InlineData("")] + [TestCase("")] public void DoubleToIntInValidValuesThrowArgumenException(object value) { var doubleToIntConverter = new DoubleToIntConverter(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/EnumToBoolConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/EnumToBoolConverter_Tests.cs new file mode 100644 index 000000000..5044ea02b --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/EnumToBoolConverter_Tests.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Xamarin.CommunityToolkit.Converters; +using NUnit.Framework; + +namespace Xamarin.CommunityToolkit.UnitTests.Converters +{ + public enum TestEnumForEnumToBoolConverter + { + None = 0, + One = 1, + Two = 2, + Three = 3, + Four = 4, + Five = 5, + Six = 6 + } + + [Flags] + public enum TestFlaggedEnumForEnumToBoolConverter + { + None = 0b0000, + One = 0b0001, + Two = 0b0010, + Three = 0b0100, + Four = 0b1000 + } + + public class EnumToBoolConverter_Tests + { + [Test] + public void EnumToBoolConvertBack_ThrowsNotImplementedException() + { + var enumToBoolConverter = new EnumToBoolConverter(); + + Assert.Throws(() => + enumToBoolConverter.ConvertBack(TestEnumForEnumToBoolConverter.Five, typeof(bool), null, CultureInfo.InvariantCulture)); + } + + [TestCase("a string")] + [TestCase(42)] + [TestCase(null)] + [TestCase(false)] + public void EnumToBoolConvert_ValueNotEnum_ThrowsArgumentException(object value) + { + var enumToBoolConverter = new EnumToBoolConverter(); + + Assert.Throws(() => + enumToBoolConverter.Convert(value, typeof(bool), TestEnumForEnumToBoolConverter.Five, CultureInfo.InvariantCulture)); + } + + [TestCase("a string")] + [TestCase(42)] + [TestCase(null)] + [TestCase(false)] + [TestCase(TestFlaggedEnumForEnumToBoolConverter.Four)] + public void EnumToBoolConvert_ParameterNotSameEnum_ReturnsFalse(object parameter) + { + var enumToBoolConverter = new EnumToBoolConverter(); + + var result = enumToBoolConverter.Convert(TestEnumForEnumToBoolConverter.Five, typeof(bool), parameter, CultureInfo.InvariantCulture); + + Assert.False(result as bool?); + } + + [TestCase(null, TestEnumForEnumToBoolConverter.Five, TestEnumForEnumToBoolConverter.Five, true)] + [TestCase(null, TestEnumForEnumToBoolConverter.Five, TestEnumForEnumToBoolConverter.Six, false)] + [TestCase(new object?[] { TestEnumForEnumToBoolConverter.Five, TestEnumForEnumToBoolConverter.Six }, TestEnumForEnumToBoolConverter.Five, TestEnumForEnumToBoolConverter.Six, true)] + [TestCase(new object?[] { TestEnumForEnumToBoolConverter.Five, TestEnumForEnumToBoolConverter.Six }, TestEnumForEnumToBoolConverter.Six, null, true)] + [TestCase(new object?[] { TestEnumForEnumToBoolConverter.Five, TestEnumForEnumToBoolConverter.Six }, TestEnumForEnumToBoolConverter.One, TestEnumForEnumToBoolConverter.Five, false)] + [TestCase(new object?[] { TestEnumForEnumToBoolConverter.Five, TestEnumForEnumToBoolConverter.Six }, TestEnumForEnumToBoolConverter.Two, null, false)] + [TestCase(new object?[] { (TestFlaggedEnumForEnumToBoolConverter.One | TestFlaggedEnumForEnumToBoolConverter.Three), TestFlaggedEnumForEnumToBoolConverter.Two }, TestFlaggedEnumForEnumToBoolConverter.One, null, true)] + [TestCase(new object?[] { (TestFlaggedEnumForEnumToBoolConverter.One | TestFlaggedEnumForEnumToBoolConverter.Three), TestFlaggedEnumForEnumToBoolConverter.Two }, TestFlaggedEnumForEnumToBoolConverter.Two, null, true)] + [TestCase(new object?[] { (TestFlaggedEnumForEnumToBoolConverter.One | TestFlaggedEnumForEnumToBoolConverter.Three), TestFlaggedEnumForEnumToBoolConverter.Two }, TestFlaggedEnumForEnumToBoolConverter.Three, null, true)] + [TestCase(new object?[] { TestFlaggedEnumForEnumToBoolConverter.One | TestFlaggedEnumForEnumToBoolConverter.Three, TestFlaggedEnumForEnumToBoolConverter.Two }, TestFlaggedEnumForEnumToBoolConverter.Four, null, false)] + [TestCase(null, TestFlaggedEnumForEnumToBoolConverter.One, TestFlaggedEnumForEnumToBoolConverter.One | TestFlaggedEnumForEnumToBoolConverter.Three, true)] + [TestCase(null, TestFlaggedEnumForEnumToBoolConverter.Three, TestFlaggedEnumForEnumToBoolConverter.One | TestFlaggedEnumForEnumToBoolConverter.Three, true)] + [TestCase(null, TestFlaggedEnumForEnumToBoolConverter.Two, TestFlaggedEnumForEnumToBoolConverter.One | TestFlaggedEnumForEnumToBoolConverter.Three, false)] + [TestCase(null, TestFlaggedEnumForEnumToBoolConverter.One | TestFlaggedEnumForEnumToBoolConverter.Three, TestFlaggedEnumForEnumToBoolConverter.One | TestFlaggedEnumForEnumToBoolConverter.Three, true)] + [TestCase(null, TestFlaggedEnumForEnumToBoolConverter.One | TestFlaggedEnumForEnumToBoolConverter.Two | TestFlaggedEnumForEnumToBoolConverter.Three, TestFlaggedEnumForEnumToBoolConverter.One | TestFlaggedEnumForEnumToBoolConverter.Three, false)] + public void EnumToBoolConvert_Validation(object?[]? trueValues, object? value, object parameter, bool expectedResult) + { + var enumToBoolConverter = new EnumToBoolConverter(); + trueValues?.OfType().ToList().ForEach(fe => enumToBoolConverter.TrueValues.Add(fe)); + + var result = enumToBoolConverter.Convert(value, typeof(bool), parameter, CultureInfo.InvariantCulture); + Assert.AreEqual(expectedResult, result); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/EqualConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/EqualConverter_Tests.cs index d82c2013c..b90f98d58 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/EqualConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/EqualConverter_Tests.cs @@ -1,6 +1,6 @@ using System.Globalization; using Xamarin.CommunityToolkit.Converters; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { @@ -8,29 +8,27 @@ public class EqualConverter_Tests { public const string TestValue = nameof(TestValue); - [Theory] - [InlineData(200, 200)] - [InlineData(TestValue, TestValue)] + [TestCase(200, 200)] + [TestCase(TestValue, TestValue)] public void IsEqual(object value, object comparedValue) { var equalConverter = new EqualConverter(); var result = equalConverter.Convert(value, typeof(EqualConverter_Tests), comparedValue, CultureInfo.CurrentCulture); - Assert.IsType(result); - Assert.True((bool)result); + Assert.IsInstanceOf(result); + Assert.IsTrue((bool)result); } - [Theory] - [InlineData(200, 400)] - [InlineData(TestValue, "")] + [TestCase(200, 400)] + [TestCase(TestValue, "")] public void IsNotEqual(object value, object comparedValue) { var equalConverter = new EqualConverter(); var result = equalConverter.Convert(value, typeof(EqualConverter_Tests), comparedValue, CultureInfo.CurrentCulture); - Assert.IsType(result); + Assert.IsInstanceOf(result); Assert.False((bool)result); } } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/IndexToArrayItemConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/IndexToArrayItemConverter_Tests.cs index 66468f228..24d25a3a8 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/IndexToArrayItemConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/IndexToArrayItemConverter_Tests.cs @@ -1,37 +1,34 @@ using System; using System.Globalization; using Xamarin.CommunityToolkit.Converters; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { public class IndexToArrayItemConverter_Tests { - [Theory] - [InlineData(new int[] { 1, 2, 3, 4, 5 }, 2, 3)] - [InlineData(new string[] { "Val1", "Val2", "Val3" }, 0, "Val1")] - [InlineData(new double[] { 1.3, 4.3, 4.3 }, 1, 4.3)] + [TestCase(new int[] { 1, 2, 3, 4, 5 }, 2, 3)] + [TestCase(new string[] { "Val1", "Val2", "Val3" }, 0, "Val1")] + [TestCase(new double[] { 1.3, 4.3, 4.3 }, 1, 4.3)] public void IndexToArrayConverter(Array value, int position, object expectedResult) { var indexToArrayConverter = new IndexToArrayItemConverter(); var result = indexToArrayConverter.Convert(position, typeof(IndexToArrayItemConverter_Tests), value, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } - [Theory] - [InlineData(null, null)] - [InlineData(null, 100)] + [TestCase(null, null)] + [TestCase(null, 100)] public void IndexToArrayInValidValuesThrowArgumenException(object value, object position) { var indexToArrayConverter = new IndexToArrayItemConverter(); Assert.Throws(() => indexToArrayConverter.Convert(position, typeof(IndexToArrayItemConverter), value, CultureInfo.CurrentCulture)); } - [Theory] - [InlineData(new int[] { 1, 2, 3, 4, 5 }, 100)] - [InlineData(new int[] { 1, 2, 3, 4, 5 }, -1)] + [TestCase(new int[] { 1, 2, 3, 4, 5 }, 100)] + [TestCase(new int[] { 1, 2, 3, 4, 5 }, -1)] public void IndexToArrayInValidValuesThrowArgumenOutOfRangeException(object value, object position) { var indexToArrayConverter = new IndexToArrayItemConverter(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/IntToBoolConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/IntToBoolConverter_Tests.cs index 6cbd3ed41..8158f03a4 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/IntToBoolConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/IntToBoolConverter_Tests.cs @@ -1,50 +1,46 @@ -using System; +using System; using System.Globalization; using Xamarin.CommunityToolkit.Converters; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { public class IntToBoolConverter_Tests { - [Theory] - [InlineData(1, true)] - [InlineData(0, false)] + [TestCase(1, true)] + [TestCase(0, false)] public void IndexToArrayConverter(int value, bool expectedResult) { var intToBoolConverter = new IntToBoolConverter(); var result = intToBoolConverter.Convert(value, typeof(IntToBoolConverter_Tests), null, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } - [Theory] - [InlineData(true, 1)] - [InlineData(false, 0)] + [TestCase(true, 1)] + [TestCase(false, 0)] public void IndexToArrayConverterBack(bool value, int expectedResult) { var intToBoolConverter = new IntToBoolConverter(); var result = intToBoolConverter.ConvertBack(value, typeof(IntToBoolConverter_Tests), null, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } - [Theory] - [InlineData(2.5)] - [InlineData("")] - [InlineData(null)] + [TestCase(2.5)] + [TestCase("")] + [TestCase(null)] public void InValidConverterValuesThrowArgumenException(object value) { var intToBoolConverter = new IntToBoolConverter(); Assert.Throws(() => intToBoolConverter.Convert(value, typeof(IndexToArrayItemConverter), null, CultureInfo.CurrentCulture)); } - [Theory] - [InlineData(2.5)] - [InlineData("")] - [InlineData(null)] + [TestCase(2.5)] + [TestCase("")] + [TestCase(null)] public void InValidConverterBackValuesThrowArgumenException(object value) { var intToBoolConverter = new IntToBoolConverter(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/InvertedBoolConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/InvertedBoolConverter_Tests.cs index 2711934ae..d599751a8 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/InvertedBoolConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/InvertedBoolConverter_Tests.cs @@ -1,28 +1,26 @@ -using System; +using System; using System.Globalization; using Xamarin.CommunityToolkit.Converters; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { public class InvertedBoolConverter_Tests { - [Theory] - [InlineData(true, false)] - [InlineData(false, true)] + [TestCase(true, false)] + [TestCase(false, true)] public void InverterBoolConverter(bool value, bool expectedResult) { var inverterBoolConverter = new InvertedBoolConverter(); var result = inverterBoolConverter.Convert(value, typeof(InvertedBoolConverter_Tests), null, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } - [Theory] - [InlineData(2)] - [InlineData("")] - [InlineData(null)] + [TestCase(2)] + [TestCase("")] + [TestCase(null)] public void InValidConverterValuesThrowArgumenException(object value) { var inverterBoolConverter = new InvertedBoolConverter(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/IsNotNullOrEmptyConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/IsNotNullOrEmptyConverter_Tests.cs index 11532b829..d7fff526d 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/IsNotNullOrEmptyConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/IsNotNullOrEmptyConverter_Tests.cs @@ -1,23 +1,22 @@ -using System.Globalization; +using System.Globalization; using Xamarin.CommunityToolkit.Converters; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { public class IsNotNullOrEmptyConverter_Tests { - [Theory] - [InlineData("Test", true)] - [InlineData(typeof(IsNotNullOrEmptyConverter), true)] - [InlineData(null, false)] - [InlineData("", false)] + [TestCase("Test", true)] + [TestCase(typeof(IsNotNullOrEmptyConverter), true)] + [TestCase(null, false)] + [TestCase("", false)] public void IsNotNullOrEmptyConverter(object value, bool expectedResult) { var isNotNullOrEmptyConverter = new IsNotNullOrEmptyConverter(); var result = isNotNullOrEmptyConverter.Convert(value, typeof(IsNotNullOrEmptyConverter_Tests), null, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/IsNullOrEmptyConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/IsNullOrEmptyConverter_Tests.cs index 662b4c7ab..2c51f8768 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/IsNullOrEmptyConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/IsNullOrEmptyConverter_Tests.cs @@ -1,23 +1,22 @@ using System.Globalization; using Xamarin.CommunityToolkit.Converters; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { public class IsNullOrEmptyConverter_Tests { - [Theory] - [InlineData(null, true)] - [InlineData("", true)] - [InlineData("Test", false)] - [InlineData(typeof(IsNullOrEmptyConverter), false)] + [TestCase(null, true)] + [TestCase("", true)] + [TestCase("Test", false)] + [TestCase(typeof(IsNullOrEmptyConverter), false)] public void IsNullOrEmptyConverter(object value, bool expectedResult) { var isNullOrEmptyConverter = new IsNullOrEmptyConverter(); var result = isNullOrEmptyConverter.Convert(value, typeof(IsNotNullOrEmptyConverter_Tests), null, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ItemSelectedEventArgsConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ItemSelectedEventArgsConverter_Tests.cs index c2e5d9cde..8ed7c3192 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ItemSelectedEventArgsConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ItemSelectedEventArgsConverter_Tests.cs @@ -3,35 +3,33 @@ using System.Globalization; using Xamarin.CommunityToolkit.Converters; using Xamarin.Forms; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { public class ItemSelectedEventArgsConverter_Tests { - static object expectedValue = 100; + static readonly object expectedValue = 100; - public static IEnumerable GetData() => new List + public static IEnumerable GetData() => new List { // We know it's deprecated, still good to test it #pragma warning disable CS0618 // Type or member is obsolete new object[] { new SelectedItemChangedEventArgs(expectedValue), expectedValue }, - new object[] { null, null }, + new object?[] { null, null }, #pragma warning restore CS0618 // Type or member is obsolete }; - [Theory] - [MemberData(nameof(GetData))] + [TestCaseSource(nameof(GetData))] public void ItemSelectedEventArgsConverter(SelectedItemChangedEventArgs value, object expectedResult) { var itemSelectedEventArgsConverter = new ItemSelectedEventArgsConverter(); var result = itemSelectedEventArgsConverter.Convert(value, typeof(ItemSelectedEventArgsConverter), null, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } - [Theory] - [InlineData("Random String")] + [TestCase("Random String")] public void InvalidConverterValuesThrowsArgumenException(object value) { var itemSelectedEventArgsConverter = new ItemSelectedEventArgsConverter(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ItemTappedEventArgsConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ItemTappedEventArgsConverter_Tests.cs index 2d825deff..7c8008042 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ItemTappedEventArgsConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ItemTappedEventArgsConverter_Tests.cs @@ -3,7 +3,7 @@ using System.Globalization; using Xamarin.CommunityToolkit.Converters; using Xamarin.Forms; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { @@ -11,28 +11,26 @@ public class ItemTappedEventArgsConverter_Tests { static object expectedValue = 100; - public static IEnumerable GetData() => new List + public static IEnumerable GetData() => new List { // We know it's deprecated, still good to test it #pragma warning disable CS0618 // Type or member is obsolete - new object[] { new ItemTappedEventArgs(null, expectedValue), expectedValue }, - new object[] { new ItemTappedEventArgs(null, null), null }, + new object?[] { new ItemTappedEventArgs(null, expectedValue), expectedValue }, + new object?[] { new ItemTappedEventArgs(null, null), null }, #pragma warning restore CS0618 // Type or member is obsolete }; - [Theory] - [MemberData(nameof(GetData))] + [TestCaseSource(nameof(GetData))] public void ItemTappedEventArgsConverter(ItemTappedEventArgs value, object expectedResult) { var itemTappedEventArgsConverter = new ItemTappedEventArgsConverter(); var result = itemTappedEventArgsConverter.Convert(value, typeof(ItemTappedEventArgsConverter), null, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } - [Theory] - [InlineData("Random String")] + [TestCase("Random String")] public void InValidConverterValuesThrowArgumenException(object value) { var itemTappedEventArgsConverter = new ItemTappedEventArgsConverter(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ListIsNotNullOrEmptyConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ListIsNotNullOrEmptyConverter_Tests.cs index 0046c4b58..f2217796d 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ListIsNotNullOrEmptyConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ListIsNotNullOrEmptyConverter_Tests.cs @@ -3,36 +3,31 @@ using System.Globalization; using System.Linq; using Xamarin.CommunityToolkit.Converters; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { public class ListIsNotNullOrEmptyConverter_Tests { - public static IEnumerable GetData() + public static IEnumerable GetData() => new List { - return new List - { - new object[] { new List(), false}, - new object[] { new List() { "TestValue"}, true}, - new object[] { null, false}, - new object[] { Enumerable.Range(1, 3), true}, - }; - } + new object[] { new List(), false }, + new object[] { new List() { "TestValue" }, true }, + new object?[] { null, false }, + new object[] { Enumerable.Range(1, 3), true }, + }; - [Theory] - [MemberData(nameof(GetData))] + [TestCaseSource(nameof(GetData))] public void ListIsNotNullOrEmptyConverter(object value, bool expectedResult) { var listIsNotNullOrEmptyConverter = new ListIsNotNullOrEmptyConverter(); var result = listIsNotNullOrEmptyConverter.Convert(value, typeof(ListIsNotNullOrEmptyConverter), null, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } - [Theory] - [InlineData(0)] + [TestCase(0)] public void InValidConverterValuesThrowArgumenException(object value) { var listIsNotNullOrEmptyConverter = new ListIsNotNullOrEmptyConverter(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ListIsNullOrEmptyConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ListIsNullOrEmptyConverter_Tests.cs index d34243e70..3762ebc29 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ListIsNullOrEmptyConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ListIsNullOrEmptyConverter_Tests.cs @@ -1,38 +1,33 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using Xamarin.CommunityToolkit.Converters; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { public class ListIsNullOrEmptyConverter_Tests { - public static IEnumerable GetData() + public static IEnumerable GetData() => new List { - return new List - { - new object[] { new List(), true}, - new object[] { new List() { "TestValue"}, false}, - new object[] { null, true}, - new object[] { Enumerable.Range(1, 3), false}, - }; - } + new object[] { new List(), true }, + new object[] { new List() { "TestValue" }, false }, + new object?[] { null, true }, + new object[] { Enumerable.Range(1, 3), false }, + }; - [Theory] - [MemberData(nameof(GetData))] + [TestCaseSource(nameof(GetData))] public void ListIsNullOrEmptyConverter(object value, bool expectedResult) { var listIstNullOrEmptyConverter = new ListIsNullOrEmptyConverter(); var result = listIstNullOrEmptyConverter.Convert(value, typeof(ListIsNullOrEmptyConverter), null, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } - [Theory] - [InlineData(0)] + [TestCase(0)] public void InValidConverterValuesThrowArgumenException(object value) { var listIstNullOrEmptyConverter = new ListIsNullOrEmptyConverter(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ListToStringConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ListToStringConverter_Tests.cs index dcd12e13a..2779ac09d 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ListToStringConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/ListToStringConverter_Tests.cs @@ -1,37 +1,34 @@ using System; using System.Collections.Generic; using Xamarin.CommunityToolkit.Converters; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { public class ListToStringConverter_Tests { - public static IEnumerable GetData() - => new List - { - new object[] { new string[] { "A", "B", "C" }, "+_+", "A+_+B+_+C" }, - new object[] { new string[] { "A", string.Empty, "C" }, ",", "A,C" }, - new object[] { new string[] { "A", null, "C" }, ",", "A,C" }, - new object[] { new string[] { "A" }, ":-:", "A" }, - new object[] { new string[] { }, ",", string.Empty }, - new object[] { null, ",", string.Empty }, - new object[] { new string[] { "A", "B", "C" }, null, "ABC" }, - }; - - [Theory] - [MemberData(nameof(GetData))] + public static IEnumerable GetData() => new List + { + new object[] { new string[] { "A", "B", "C" }, "+_+", "A+_+B+_+C" }, + new object[] { new string[] { "A", string.Empty, "C" }, ",", "A,C" }, + new object?[] { new string?[] { "A", null, "C" }, ",", "A,C" }, + new object[] { new string[] { "A" }, ":-:", "A" }, + new object[] { new string[] { }, ",", string.Empty }, + new object?[] { null, ",", string.Empty }, + new object?[] { new string[] { "A", "B", "C" }, null, "ABC" }, + }; + + [TestCaseSource(nameof(GetData))] public void ListToStringConverter(object value, object parameter, object expectedResult) { var listToStringConverter = new ListToStringConverter(); var result = listToStringConverter.Convert(value, null, parameter, null); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } - [Theory] - [InlineData(0)] + [TestCase(0)] public void InValidConverterValuesThrowArgumenException(object value) { var listToStringConverter = new ListToStringConverter(); @@ -39,8 +36,7 @@ public void InValidConverterValuesThrowArgumenException(object value) Assert.Throws(() => listToStringConverter.Convert(value, null, null, null)); } - [Theory] - [InlineData(0)] + [TestCase(0)] public void InValidConverterParametersThrowArgumenException(object parameter) { var listToStringConverter = new ListToStringConverter(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/MultiConverterParameter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/MultiConverterParameter_Tests.cs index 6ba05a1c4..dadd8cc77 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/MultiConverterParameter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/MultiConverterParameter_Tests.cs @@ -1,5 +1,4 @@ -using System; -namespace Xamarin.CommunityToolkit.UnitTests.Converters +namespace Xamarin.CommunityToolkit.UnitTests.Converters { public class MultiConverterParameter_Tests { diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/MultiConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/MultiConverter_Tests.cs index 33bb1bbb4..78bdb4971 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/MultiConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/MultiConverter_Tests.cs @@ -1,29 +1,25 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Globalization; using Xamarin.CommunityToolkit.Converters; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { public class MultiConverter_Tests { - public static IEnumerable GetData() + public static IEnumerable GetData() => new List { - return new List - { - new object[] { new List() { { new MultiConverterParameter() { Value = "Param 1", } } , { new MultiConverterParameter() { Value = "Param 2", } } }}, - }; - } + new object[] { new List() { { new MultiConverterParameter() { Value = "Param 1", } }, { new MultiConverterParameter() { Value = "Param 2", } } } }, + }; - [Theory] - [MemberData(nameof(GetData))] + [TestCaseSource(nameof(GetData))] public void MultiConverter(object value) { var multiConverter = new MultiConverter(); var result = multiConverter.Convert(value, typeof(MultiConverter), null, CultureInfo.CurrentCulture); - Assert.Equal(result, value); + Assert.AreEqual(result, value); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/NotEqualConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/NotEqualConverter_Tests.cs index 6d7027248..0faad61c4 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/NotEqualConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/NotEqualConverter_Tests.cs @@ -1,23 +1,22 @@ -using System.Globalization; +using System.Globalization; using Xamarin.CommunityToolkit.Converters; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { public class NotEqualConverter_Tests { - [Theory] - [InlineData(true, true, false)] - [InlineData(int.MaxValue, int.MinValue, true)] - [InlineData("Test", true, true)] - [InlineData(null, null, false)] + [TestCase(true, true, false)] + [TestCase(int.MaxValue, int.MinValue, true)] + [TestCase("Test", true, true)] + [TestCase(null, null, false)] public void NotEqualConverter(object value, object comparedValue, bool expectedResult) { var notEqualConverter = new NotEqualConverter(); var result = notEqualConverter.Convert(value, typeof(NotEqualConverter_Tests), comparedValue, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/TextCaseConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/TextCaseConverter_Tests.cs index d338adf62..d22cd6785 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/TextCaseConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/TextCaseConverter_Tests.cs @@ -1,7 +1,8 @@ -using System; +using System.Collections.Generic; using System.Globalization; +using NUnit.Framework; using Xamarin.CommunityToolkit.Converters; -using Xunit; +using Xamarin.CommunityToolkit.UnitTests.Mocks; namespace Xamarin.CommunityToolkit.UnitTests.Converters { @@ -10,32 +11,46 @@ public class TextCaseConverter_Tests const string test = nameof(test); const string t = nameof(t); - [Theory] - [InlineData(test, TextCaseType.Lower, test)] - [InlineData(test, TextCaseType.Upper, "TEST")] - [InlineData(test, TextCaseType.None, test)] - [InlineData(t, TextCaseType.Upper, "T")] - [InlineData(t, TextCaseType.Lower, t)] - [InlineData(t, TextCaseType.None, t)] - [InlineData(null, null, null)] - public void TextCaseConverter(object value, object comparedValue, object expectedResult) + static IEnumerable GetTestData() + { + yield return new object?[] { test, TextCaseType.Lower, test }; + yield return new object?[] { test, TextCaseType.Upper, "TEST" }; + yield return new object?[] { test, TextCaseType.None, test }; + yield return new object?[] { test, TextCaseType.FirstUpperRestLower, "Test" }; + yield return new object?[] { t, TextCaseType.Upper, "T" }; + yield return new object?[] { t, TextCaseType.Lower, t }; + yield return new object?[] { t, TextCaseType.None, t }; + yield return new object?[] { t, TextCaseType.FirstUpperRestLower, "T" }; + yield return new object?[] { string.Empty, TextCaseType.FirstUpperRestLower, string.Empty }; + yield return new object?[] { null, TextCaseType.None, null }; + yield return new object?[] { MockEnum.Foo, TextCaseType.Lower, "foo" }; + yield return new object?[] { MockEnum.Bar, TextCaseType.None, "Bar" }; + yield return new object?[] { MockEnum.Baz, TextCaseType.Upper, "BAZ" }; + yield return new object?[] { new MockItem { Title = "Test Item", Completed = true }, TextCaseType.Upper, "TEST ITEM IS COMPLETED" }; + } + + [TestCaseSource(nameof(GetTestData))] + [TestCase(null, null, null)] + public void TextCaseConverterWithParameter(object? value, object? comparedValue, object? expectedResult) { var textCaseConverter = new TextCaseConverter(); - var result = textCaseConverter.Convert(value, typeof(TextCaseConverter_Tests), comparedValue, CultureInfo.CurrentCulture); + var result = textCaseConverter.Convert(value, typeof(string), comparedValue, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } - [Theory] - [InlineData(0)] - [InlineData(int.MinValue)] - [InlineData(double.MaxValue)] - public void InValidConverterValuesThrowArgumenException(object value) + [TestCaseSource(nameof(GetTestData))] + public void TextCaseConverterWithExplicitType(object? value, TextCaseType textCaseType, object? expectedResult) { - var textCaseConverter = new TextCaseConverter(); + var textCaseConverter = new TextCaseConverter + { + Type = textCaseType + }; + + var result = textCaseConverter.Convert(value, typeof(string), null, CultureInfo.CurrentCulture); - Assert.Throws(() => textCaseConverter.Convert(value, typeof(TextCaseConverter_Tests), null, CultureInfo.CurrentCulture)); + Assert.AreEqual(result, expectedResult); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/VariableMultiValueConverter_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/VariableMultiValueConverter_Tests.cs index ff91f159a..6ae3613b4 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/VariableMultiValueConverter_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Converters/VariableMultiValueConverter_Tests.cs @@ -1,42 +1,41 @@ using System.Globalization; using Xamarin.CommunityToolkit.Converters; using Xamarin.CommunityToolkit.Helpers; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Converters { public class VariableMultiValueConverter_Tests { - [Theory] - [InlineData(new object[] { true, true, true }, MultiBindingCondition.None, false)] - [InlineData(new object[] { true, false, true }, MultiBindingCondition.None, false)] - [InlineData(new object[] { false, false, false }, MultiBindingCondition.None, true)] - [InlineData(new object[] { true, true, true }, MultiBindingCondition.All, true)] - [InlineData(new object[] { true, false, true }, MultiBindingCondition.All, false)] - [InlineData(new object[] { false, false, false }, MultiBindingCondition.All, false)] - [InlineData(new object[] { true, true, true }, MultiBindingCondition.Any, true)] - [InlineData(new object[] { false, false, false }, MultiBindingCondition.Any, false)] - [InlineData(new object[] { false, true, false }, MultiBindingCondition.Any, true)] - [InlineData(new object[] { true, true, true }, MultiBindingCondition.Exact, true, 3)] - [InlineData(new object[] { false, false, false }, MultiBindingCondition.Exact, false, 1)] - [InlineData(new object[] { false, true, false }, MultiBindingCondition.Exact, true, 1)] - [InlineData(new object[] { true, true, true }, MultiBindingCondition.GreaterThan, true, 2)] - [InlineData(new object[] { false, false, false }, MultiBindingCondition.GreaterThan, false, 1)] - [InlineData(new object[] { false, true, false }, MultiBindingCondition.GreaterThan, true, 0)] - [InlineData(new object[] { true, true, true }, MultiBindingCondition.LessThan, false, 2)] - [InlineData(new object[] { false, false, false }, MultiBindingCondition.LessThan, true, 1)] - [InlineData(new object[] { false, true, false }, MultiBindingCondition.LessThan, true, 2)] - [InlineData(null, MultiBindingCondition.All, false)] - [InlineData(null, MultiBindingCondition.Any, false)] - [InlineData(null, MultiBindingCondition.Exact, false)] - [InlineData(null, MultiBindingCondition.GreaterThan, false)] - [InlineData(null, MultiBindingCondition.LessThan, false)] - [InlineData(null, MultiBindingCondition.None, false)] + [TestCase(new object[] { true, true, true }, MultiBindingCondition.None, false)] + [TestCase(new object[] { true, false, true }, MultiBindingCondition.None, false)] + [TestCase(new object[] { false, false, false }, MultiBindingCondition.None, true)] + [TestCase(new object[] { true, true, true }, MultiBindingCondition.All, true)] + [TestCase(new object[] { true, false, true }, MultiBindingCondition.All, false)] + [TestCase(new object[] { false, false, false }, MultiBindingCondition.All, false)] + [TestCase(new object[] { true, true, true }, MultiBindingCondition.Any, true)] + [TestCase(new object[] { false, false, false }, MultiBindingCondition.Any, false)] + [TestCase(new object[] { false, true, false }, MultiBindingCondition.Any, true)] + [TestCase(new object[] { true, true, true }, MultiBindingCondition.Exact, true, 3)] + [TestCase(new object[] { false, false, false }, MultiBindingCondition.Exact, false, 1)] + [TestCase(new object[] { false, true, false }, MultiBindingCondition.Exact, true, 1)] + [TestCase(new object[] { true, true, true }, MultiBindingCondition.GreaterThan, true, 2)] + [TestCase(new object[] { false, false, false }, MultiBindingCondition.GreaterThan, false, 1)] + [TestCase(new object[] { false, true, false }, MultiBindingCondition.GreaterThan, true, 0)] + [TestCase(new object[] { true, true, true }, MultiBindingCondition.LessThan, false, 2)] + [TestCase(new object[] { false, false, false }, MultiBindingCondition.LessThan, true, 1)] + [TestCase(new object[] { false, true, false }, MultiBindingCondition.LessThan, true, 2)] + [TestCase(null, MultiBindingCondition.All, false)] + [TestCase(null, MultiBindingCondition.Any, false)] + [TestCase(null, MultiBindingCondition.Exact, false)] + [TestCase(null, MultiBindingCondition.GreaterThan, false)] + [TestCase(null, MultiBindingCondition.LessThan, false)] + [TestCase(null, MultiBindingCondition.None, false)] public void VariableMultiConverter(object[] value, MultiBindingCondition type, object expectedResult, int count = 0) { var variableMultiConverter = new VariableMultiValueConverter() { ConditionType = type, Count = count }; var result = variableMultiConverter.Convert(value, typeof(bool), null, CultureInfo.CurrentCulture); - Assert.Equal(result, expectedResult); + Assert.AreEqual(result, expectedResult); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/EnableInitOnlyProperties.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/EnableInitOnlyProperties.cs new file mode 100644 index 000000000..f3a7fee73 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/EnableInitOnlyProperties.cs @@ -0,0 +1,4 @@ +namespace System.Runtime.CompilerServices +{ + public record IsExternalInit; +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/LocalizationResourceManagerTests/LocalizationResourceManagerTests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/LocalizationResourceManagerTests/LocalizationResourceManagerTests.cs index 2a8b6bb6b..c49020b27 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/LocalizationResourceManagerTests/LocalizationResourceManagerTests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/LocalizationResourceManagerTests/LocalizationResourceManagerTests.cs @@ -2,26 +2,34 @@ using System.Resources; using Xamarin.CommunityToolkit.Helpers; using Xamarin.CommunityToolkit.UnitTests.Mocks; -using Xunit; +using NUnit.Framework; +using System; -namespace Xamarin.CommunityToolkit.UnitTests.Helpers.WeakEventManagerTests +namespace Xamarin.CommunityToolkit.UnitTests.Helpers.LocalizationResourceManagerTests { + [NonParallelizable] public class LocalizationResourceManagerTests { - public LocalizationResourceManagerTests() + ResourceManager? resourceManager; + CultureInfo? initialCulture; + LocalizationResourceManager? localizationManager; + + [SetUp] + public void Setup() { resourceManager = new MockResourceManager(); + initialCulture = CultureInfo.InvariantCulture; localizationManager = LocalizationResourceManager.Current; + localizationManager.Init(resourceManager, initialCulture); } - readonly LocalizationResourceManager localizationManager; - readonly ResourceManager resourceManager; - readonly CultureInfo initialCulture = CultureInfo.InvariantCulture; - - [Fact] + [Test] public void LocalizationResourceManager_GetCulture_Equal_Indexer() { + _ = localizationManager ?? throw new NullReferenceException(); + _ = resourceManager ?? throw new NullReferenceException(); + // Arrange var testString = "test"; var culture2 = new CultureInfo("en"); @@ -30,34 +38,38 @@ public void LocalizationResourceManager_GetCulture_Equal_Indexer() var responceIndexerCulture1 = localizationManager[testString]; var responceGetValueCulture1 = localizationManager.GetValue(testString); var responceResourceManagerCulture1 = resourceManager.GetString(testString, initialCulture); - localizationManager.SetCulture(culture2); + + localizationManager.CurrentCulture = culture2; var responceIndexerCulture2 = localizationManager[testString]; var responceGetValueCulture2 = localizationManager.GetValue(testString); var responceResourceManagerCulture2 = resourceManager.GetString(testString, culture2); - resourceManager.GetString(testString, culture2); // Assert - Assert.Equal(responceResourceManagerCulture1, responceIndexerCulture1); - Assert.Equal(responceResourceManagerCulture1, responceGetValueCulture1); - Assert.Equal(responceResourceManagerCulture2, responceIndexerCulture2); - Assert.Equal(responceResourceManagerCulture2, responceGetValueCulture2); + Assert.AreEqual(responceResourceManagerCulture1, responceIndexerCulture1); + Assert.AreEqual(responceResourceManagerCulture1, responceGetValueCulture1); + Assert.AreEqual(responceResourceManagerCulture2, responceIndexerCulture2); + Assert.AreEqual(responceResourceManagerCulture2, responceGetValueCulture2); } - [Fact] + [Test] public void LocalizationResourceManager_PropertyChanged_Triggered() { + _ = initialCulture ?? throw new NullReferenceException(); + _ = resourceManager ?? throw new NullReferenceException(); + _ = localizationManager ?? throw new NullReferenceException(); + // Arrange var culture2 = new CultureInfo("en"); - localizationManager.SetCulture(culture2); - CultureInfo changedCulture = null; + CultureInfo? changedCulture = null; + localizationManager.CurrentCulture = culture2; localizationManager.PropertyChanged += (s, e) => changedCulture = localizationManager.CurrentCulture; // Act, Assert localizationManager.Init(resourceManager, initialCulture); - Assert.Equal(initialCulture, changedCulture); + Assert.AreEqual(initialCulture, changedCulture); - localizationManager.SetCulture(culture2); - Assert.Equal(culture2, changedCulture); + localizationManager.CurrentCulture = culture2; + Assert.AreEqual(culture2, changedCulture); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/LocalizedStringTests/LocalizedStringTests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/LocalizedStringTests/LocalizedStringTests.cs new file mode 100644 index 000000000..fd6038010 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/LocalizedStringTests/LocalizedStringTests.cs @@ -0,0 +1,119 @@ +using System; +using System.Globalization; +using System.Resources; +using Xamarin.CommunityToolkit.Helpers; +using Xamarin.CommunityToolkit.UnitTests.Mocks; +using NUnit.Framework; + +namespace Xamarin.CommunityToolkit.UnitTests.Helpers.LocalizedStringTests +{ + [NonParallelizable] + public class LocalizedStringTests + { + CultureInfo? initialCulture; + ResourceManager? resourceManager; + LocalizationResourceManager? localizationManager; + + LocalizedString? localizedString; + + [SetUp] + public void Setup() + { + resourceManager = new MockResourceManager(); + initialCulture = CultureInfo.InvariantCulture; + localizationManager = LocalizationResourceManager.Current; + + localizationManager.Init(resourceManager, initialCulture); + } + + [Test] + public void LocalizedStringTests_Localized_ValidImplementation() + { + _ = initialCulture ?? throw new NullReferenceException(); + _ = resourceManager ?? throw new NullReferenceException(); + _ = localizationManager ?? throw new NullReferenceException(); + + // Arrange + var testString = "test"; + var culture2 = new CultureInfo("en"); + localizedString = new LocalizedString(localizationManager, () => localizationManager[testString]); + + string? responceOnCultureChanged = null; + localizedString.PropertyChanged += (sender, args) => responceOnCultureChanged = localizedString.Localized; + + // Act + var responceCulture1 = localizedString.Localized; + var responceResourceManagerCulture1 = resourceManager.GetString(testString, initialCulture); + localizationManager.CurrentCulture = culture2; + var responceCulture2 = localizedString.Localized; + var responceResourceManagerCulture2 = resourceManager.GetString(testString, culture2); + + // Assert + Assert.AreEqual(responceResourceManagerCulture1, responceCulture1); + Assert.AreEqual(responceResourceManagerCulture2, responceOnCultureChanged); + Assert.AreEqual(responceResourceManagerCulture2, responceResourceManagerCulture2); + } + + [Test] + public void LocalizedStringTests_ImplicitConversion_ValidImplementation() + { + // Arrange + var testString = "test"; + Func generator = () => testString; + + // Act + localizedString = generator; + + // Assert + Assert.IsNotNull(localizedString); + } + + [Test] + public void LocalizedStringTests_WeekSubscribe_ValidImplementation() + { + _ = localizationManager ?? throw new NullReferenceException(); + + // Arrange + var isTrigered = false; + var culture2 = new CultureInfo("en"); + localizedString = new LocalizedString(localizationManager, () => string.Empty); + localizedString.PropertyChanged += (_, __) => isTrigered = true; + + // Act + GC.Collect(); + localizationManager.CurrentCulture = culture2; + + // Assert + Assert.IsTrue(isTrigered); + } + +#if NET461 +#warning Test fails on mono x64 Running on macOS +#else + [Test] + public void LocalizedStringTests_Disposed_IfNoReferences() + { + _ = initialCulture ?? throw new NullReferenceException(); + _ = resourceManager ?? throw new NullReferenceException(); + _ = localizationManager ?? throw new NullReferenceException(); + + // Arrange + var testString = "test"; + SetLocalizedString(); + var weaklocalizedString = new WeakReference(localizedString); + localizedString = null; + + // Act + GC.Collect(); + + // Assert + Assert.False(weaklocalizedString.IsAlive); + + void SetLocalizedString() + { + localizedString = new LocalizedString(localizationManager, () => localizationManager[testString]); + } + } +#endif + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/DelegateWeakEventManager_Action_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/DelegateWeakEventManager_Action_Tests.cs index fe5b217df..6763b2271 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/DelegateWeakEventManager_Action_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/DelegateWeakEventManager_Action_Tests.cs @@ -1,7 +1,7 @@ using System; using Xamarin.CommunityToolkit.Exceptions; using Xamarin.CommunityToolkit.Helpers; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Helpers.WeakEventManagerTests { @@ -15,7 +15,7 @@ public class DelegateWeakEventManager_Action_Tests : BaseWeakEventManagerTests remove => actionEventManager.RemoveEventHandler(value); } - [Fact] + [Test] public void WeakEventManagerAction_HandleEvent_ValidImplementation() { // Arrange @@ -32,10 +32,10 @@ void HandleDelegateTest() actionEventManager.RaiseEvent(nameof(ActionEvent)); // Assert - Assert.True(didEventFire); + Assert.IsTrue(didEventFire); } - [Fact] + [Test] public void WeakEventManagerAction_HandleEvent_InvalidHandleEventEventName() { // Arrange @@ -52,7 +52,7 @@ public void WeakEventManagerAction_HandleEvent_InvalidHandleEventEventName() ActionEvent -= HandleDelegateTest; } - [Fact] + [Test] public void WeakEventManagerAction_UnassignedEvent() { // Arrange @@ -69,7 +69,7 @@ public void WeakEventManagerAction_UnassignedEvent() Assert.False(didEventFire); } - [Fact] + [Test] public void WeakEventManagerAction_UnassignedEventManager() { // Arrange @@ -87,7 +87,7 @@ public void WeakEventManagerAction_UnassignedEventManager() ActionEvent -= HandleDelegateTest; } - [Fact] + [Test] public void WeakEventManagerAction_HandleEvent_InvalidHandleEvent() { // Arrange @@ -104,7 +104,7 @@ public void WeakEventManagerAction_HandleEvent_InvalidHandleEvent() ActionEvent -= HandleDelegateTest; } - [Fact] + [Test] public void WeakEventManagerAction_AddEventHandler_NullHandler() { // Arrange @@ -117,7 +117,7 @@ public void WeakEventManagerAction_AddEventHandler_NullHandler() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManagerAction_AddEventHandler_NullEventName() { // Arrange @@ -130,7 +130,7 @@ public void WeakEventManagerAction_AddEventHandler_NullEventName() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManagerAction_AddEventHandler_EmptyEventName() { // Arrange @@ -143,7 +143,7 @@ public void WeakEventManagerAction_AddEventHandler_EmptyEventName() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManagerAction_AddEventHandler_WhitespaceEventName() { // Arrange @@ -156,7 +156,7 @@ public void WeakEventManagerAction_AddEventHandler_WhitespaceEventName() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManagerAction_RemoveEventHandler_NullHandler() { // Arrange @@ -169,7 +169,7 @@ public void WeakEventManagerAction_RemoveEventHandler_NullHandler() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManagerAction_RemoveEventHandler_NullEventName() { // Arrange @@ -182,7 +182,7 @@ public void WeakEventManagerAction_RemoveEventHandler_NullEventName() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManagerAction_RemoveEventHandler_EmptyEventName() { // Arrange @@ -195,7 +195,7 @@ public void WeakEventManagerAction_RemoveEventHandler_EmptyEventName() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManagerAction_RemoveEventHandler_WhiteSpaceEventName() { // Arrange diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/DelegateWeakEventManager_Delegate_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/DelegateWeakEventManager_Delegate_Tests.cs index 417cca45c..4645db3ee 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/DelegateWeakEventManager_Delegate_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/DelegateWeakEventManager_Delegate_Tests.cs @@ -2,7 +2,7 @@ using System.ComponentModel; using Xamarin.CommunityToolkit.Exceptions; using Xamarin.CommunityToolkit.Helpers; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Helpers.WeakEventManagerTests { @@ -16,19 +16,19 @@ public class WeakEventManager_Delegate_Tests : BaseWeakEventManagerTests, INotif remove => propertyChangedWeakEventManager.RemoveEventHandler(value); } - [Fact] + [Test] public void WeakEventManagerDelegate_HandleEvent_ValidImplementation() { // Arrange PropertyChanged += HandleDelegateTest; var didEventFire = false; - void HandleDelegateTest(object sender, PropertyChangedEventArgs e) + void HandleDelegateTest(object? sender, PropertyChangedEventArgs e) { - Assert.NotNull(sender); - Assert.Equal(GetType(), sender.GetType()); + Assert.IsNotNull(sender); + Assert.AreEqual(GetType(), sender?.GetType()); - Assert.NotNull(e); + Assert.IsNotNull(e); didEventFire = true; PropertyChanged -= HandleDelegateTest; @@ -38,40 +38,42 @@ void HandleDelegateTest(object sender, PropertyChangedEventArgs e) propertyChangedWeakEventManager.RaiseEvent(this, new PropertyChangedEventArgs("Test"), nameof(PropertyChanged)); // Assert - Assert.True(didEventFire); + Assert.IsTrue(didEventFire); } - [Fact] + [Test] public void WeakEventManagerDelegate_HandleEvent_NullSender() { // Arrange PropertyChanged += HandleDelegateTest; var didEventFire = false; - void HandleDelegateTest(object sender, PropertyChangedEventArgs e) + void HandleDelegateTest(object? sender, PropertyChangedEventArgs e) { Assert.Null(sender); - Assert.NotNull(e); + Assert.IsNotNull(e); didEventFire = true; PropertyChanged -= HandleDelegateTest; } // Act +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. propertyChangedWeakEventManager.RaiseEvent(null, new PropertyChangedEventArgs("Test"), nameof(PropertyChanged)); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. // Assert - Assert.True(didEventFire); + Assert.IsTrue(didEventFire); } - [Fact] + [Test] public void WeakEventManagerDelegate_HandleEvent_InvalidEventArgs() { // Arrange PropertyChanged += HandleDelegateTest; var didEventFire = false; - void HandleDelegateTest(object sender, PropertyChangedEventArgs e) => didEventFire = true; + void HandleDelegateTest(object? sender, PropertyChangedEventArgs e) => didEventFire = true; // Act @@ -81,17 +83,17 @@ public void WeakEventManagerDelegate_HandleEvent_InvalidEventArgs() PropertyChanged -= HandleDelegateTest; } - [Fact] + [Test] public void WeakEventManagerDelegate_HandleEvent_NullEventArgs() { // Arrange PropertyChanged += HandleDelegateTest; var didEventFire = false; - void HandleDelegateTest(object sender, PropertyChangedEventArgs e) + void HandleDelegateTest(object? sender, PropertyChangedEventArgs e) { - Assert.NotNull(sender); - Assert.Equal(GetType(), sender.GetType()); + Assert.IsNotNull(sender); + Assert.AreEqual(GetType(), sender?.GetType()); Assert.Null(e); @@ -105,17 +107,17 @@ void HandleDelegateTest(object sender, PropertyChangedEventArgs e) #pragma warning restore CS8625 //Cannot convert null literal to non-nullable reference type // Assert - Assert.True(didEventFire); + Assert.IsTrue(didEventFire); } - [Fact] + [Test] public void WeakEventManagerDelegate_HandleEvent_InvalidHandleEventEventName() { // Arrange PropertyChanged += HandleDelegateTest; var didEventFire = false; - void HandleDelegateTest(object sender, PropertyChangedEventArgs e) => didEventFire = true; + void HandleDelegateTest(object? sender, PropertyChangedEventArgs e) => didEventFire = true; // Act propertyChangedWeakEventManager.RaiseEvent(this, new PropertyChangedEventArgs("Test"), nameof(TestStringEvent)); @@ -125,7 +127,7 @@ public void WeakEventManagerDelegate_HandleEvent_InvalidHandleEventEventName() PropertyChanged -= HandleDelegateTest; } - [Fact] + [Test] public void WeakEventManagerDelegate_HandleEvent_DynamicMethod_ValidImplementation() { // Arrange @@ -143,7 +145,7 @@ public void WeakEventManagerDelegate_HandleEvent_DynamicMethod_ValidImplementati PropertyChanged -= handler; } - [Fact] + [Test] public void WeakEventManagerDelegate_UnassignedEvent() { // Arrange @@ -151,7 +153,7 @@ public void WeakEventManagerDelegate_UnassignedEvent() PropertyChanged += HandleDelegateTest; PropertyChanged -= HandleDelegateTest; - void HandleDelegateTest(object sender, PropertyChangedEventArgs e) => didEventFire = true; + void HandleDelegateTest(object? sender, PropertyChangedEventArgs e) => didEventFire = true; // Act #pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type @@ -162,7 +164,7 @@ public void WeakEventManagerDelegate_UnassignedEvent() Assert.False(didEventFire); } - [Fact] + [Test] public void WeakEventManagerDelegate_UnassignedEventManager() { // Arrange @@ -170,24 +172,26 @@ public void WeakEventManagerDelegate_UnassignedEventManager() var didEventFire = false; PropertyChanged += HandleDelegateTest; - void HandleDelegateTest(object sender, PropertyChangedEventArgs e) => didEventFire = true; + void HandleDelegateTest(object? sender, PropertyChangedEventArgs e) => didEventFire = true; // Act +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. unassignedEventManager.RaiseEvent(null, null, nameof(PropertyChanged)); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. // Assert Assert.False(didEventFire); PropertyChanged -= HandleDelegateTest; } - [Fact] + [Test] public void WeakEventManagerDelegate_HandleEvent_InvalidHandleEvent() { // Arrange PropertyChanged += HandleDelegateTest; var didEventFire = false; - void HandleDelegateTest(object sender, PropertyChangedEventArgs e) => didEventFire = true; + void HandleDelegateTest(object? sender, PropertyChangedEventArgs e) => didEventFire = true; // Act @@ -197,7 +201,7 @@ public void WeakEventManagerDelegate_HandleEvent_InvalidHandleEvent() PropertyChanged -= HandleDelegateTest; } - [Fact] + [Test] public void WeakEventManagerDelegate_AddEventHandler_NullHandler() { // Arrange @@ -205,12 +209,10 @@ public void WeakEventManagerDelegate_AddEventHandler_NullHandler() // Act // Assert -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference Assert.Throws(() => propertyChangedWeakEventManager.AddEventHandler(null)); -#pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManagerDelegate_AddEventHandler_NullEventName() { // Arrange @@ -223,7 +225,7 @@ public void WeakEventManagerDelegate_AddEventHandler_NullEventName() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManagerDelegate_AddEventHandler_EmptyEventName() { // Arrange @@ -231,12 +233,10 @@ public void WeakEventManagerDelegate_AddEventHandler_EmptyEventName() // Act // Assert -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference Assert.Throws(() => propertyChangedWeakEventManager.AddEventHandler(null, string.Empty)); -#pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManagerDelegate_AddEventHandler_WhitespaceEventName() { // Arrange @@ -244,12 +244,10 @@ public void WeakEventManagerDelegate_AddEventHandler_WhitespaceEventName() // Act // Assert -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference Assert.Throws(() => propertyChangedWeakEventManager.AddEventHandler(null, " ")); -#pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManagerDelegate_RemoveEventHandler_NullHandler() { // Arrange @@ -257,12 +255,10 @@ public void WeakEventManagerDelegate_RemoveEventHandler_NullHandler() // Act // Assert -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference Assert.Throws(() => propertyChangedWeakEventManager.RemoveEventHandler(null)); -#pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManagerDelegate_RemoveEventHandler_NullEventName() { // Arrange @@ -275,7 +271,7 @@ public void WeakEventManagerDelegate_RemoveEventHandler_NullEventName() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManagerDelegate_RemoveEventHandler_EmptyEventName() { // Arrange @@ -283,12 +279,10 @@ public void WeakEventManagerDelegate_RemoveEventHandler_EmptyEventName() // Act // Assert -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference Assert.Throws(() => propertyChangedWeakEventManager.RemoveEventHandler(null, string.Empty)); -#pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManagerDelegate_RemoveEventHandler_WhiteSpaceEventName() { // Arrange @@ -296,9 +290,7 @@ public void WeakEventManagerDelegate_RemoveEventHandler_WhiteSpaceEventName() // Act // Assert -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference Assert.Throws(() => propertyChangedWeakEventManager.RemoveEventHandler(null, " ")); -#pragma warning restore CS8625 } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/DelegateWeakEventManager_EventHandler_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/DelegateWeakEventManager_EventHandler_Tests.cs index aabdcea8a..f99448db1 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/DelegateWeakEventManager_EventHandler_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/DelegateWeakEventManager_EventHandler_Tests.cs @@ -1,13 +1,13 @@ using System; using Xamarin.CommunityToolkit.Exceptions; using Xamarin.CommunityToolkit.Helpers; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Helpers.WeakEventManagerTests { public class DelegateWeakEventManager_EventHandler_Tests : BaseWeakEventManagerTests { - [Fact] + [Test] public void WeakEventManager_HandleEvent_ValidImplementation() { // Arrange @@ -19,10 +19,10 @@ void HandleTestEvent(object? sender, EventArgs e) if (sender == null) throw new ArgumentNullException(nameof(sender)); - Assert.NotNull(sender); - Assert.Equal(GetType(), sender.GetType()); + Assert.IsNotNull(sender); + Assert.AreEqual(GetType(), sender.GetType()); - Assert.NotNull(e); + Assert.IsNotNull(e); didEventFire = true; TestEvent -= HandleTestEvent; @@ -32,10 +32,10 @@ void HandleTestEvent(object? sender, EventArgs e) TestWeakEventManager.RaiseEvent(this, new EventArgs(), nameof(TestEvent)); // Assert - Assert.True(didEventFire); + Assert.IsTrue(didEventFire); } - [Fact] + [Test] public void WeakEventManager_HandleEvent_NullSender() { // Arrange @@ -45,20 +45,22 @@ public void WeakEventManager_HandleEvent_NullSender() void HandleTestEvent(object? sender, EventArgs e) { Assert.Null(sender); - Assert.NotNull(e); + Assert.IsNotNull(e); didEventFire = true; TestEvent -= HandleTestEvent; } // Act +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. TestWeakEventManager.RaiseEvent(null, new EventArgs(), nameof(TestEvent)); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. // Assert - Assert.True(didEventFire); + Assert.IsTrue(didEventFire); } - [Fact] + [Test] public void WeakEventManager_HandleEvent_EmptyEventArgs() { // Arrange @@ -70,11 +72,11 @@ void HandleTestEvent(object? sender, EventArgs e) if (sender == null) throw new ArgumentNullException(nameof(sender)); - Assert.NotNull(sender); - Assert.Equal(GetType(), sender.GetType()); + Assert.IsNotNull(sender); + Assert.AreEqual(GetType(), sender.GetType()); - Assert.NotNull(e); - Assert.Equal(EventArgs.Empty, e); + Assert.IsNotNull(e); + Assert.AreEqual(EventArgs.Empty, e); didEventFire = true; TestEvent -= HandleTestEvent; @@ -84,10 +86,10 @@ void HandleTestEvent(object? sender, EventArgs e) TestWeakEventManager.RaiseEvent(this, EventArgs.Empty, nameof(TestEvent)); // Assert - Assert.True(didEventFire); + Assert.IsTrue(didEventFire); } - [Fact] + [Test] public void WeakEventManager_HandleEvent_NullEventArgs() { // Arrange @@ -99,8 +101,8 @@ void HandleTestEvent(object? sender, EventArgs e) if (sender == null) throw new ArgumentNullException(nameof(sender)); - Assert.NotNull(sender); - Assert.Equal(GetType(), sender.GetType()); + Assert.IsNotNull(sender); + Assert.AreEqual(GetType(), sender.GetType()); Assert.Null(e); @@ -114,10 +116,10 @@ void HandleTestEvent(object? sender, EventArgs e) #pragma warning restore CS8625 // Assert - Assert.True(didEventFire); + Assert.IsTrue(didEventFire); } - [Fact] + [Test] public void WeakEventManager_HandleEvent_InvalidHandleEventName() { // Arrange @@ -134,7 +136,7 @@ public void WeakEventManager_HandleEvent_InvalidHandleEventName() TestEvent -= HandleTestEvent; } - [Fact] + [Test] public void WeakEventManager_UnassignedEvent() { // Arrange @@ -145,13 +147,15 @@ public void WeakEventManager_UnassignedEvent() void HandleTestEvent(object? sender, EventArgs e) => didEventFire = true; // Act +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. TestWeakEventManager.RaiseEvent(null, null, nameof(TestEvent)); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. // Assert Assert.False(didEventFire); } - [Fact] + [Test] public void WeakEventManager_UnassignedEventManager() { // Arrange @@ -162,14 +166,16 @@ public void WeakEventManager_UnassignedEventManager() void HandleTestEvent(object? sender, EventArgs e) => didEventFire = true; // Act +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. unassignedEventManager.RaiseEvent(null, null, nameof(TestEvent)); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. // Assert Assert.False(didEventFire); TestEvent -= HandleTestEvent; } - [Fact] + [Test] public void WeakEventManager_AddEventHandler_NullHandler() { // Arrange @@ -182,7 +188,7 @@ public void WeakEventManager_AddEventHandler_NullHandler() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManager_AddEventHandler_NullEventName() { // Arrange @@ -195,7 +201,7 @@ public void WeakEventManager_AddEventHandler_NullEventName() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManager_AddEventHandler_EmptyEventName() { // Arrange @@ -208,7 +214,7 @@ public void WeakEventManager_AddEventHandler_EmptyEventName() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManager_AddEventHandler_WhitespaceEventName() { // Arrange @@ -221,7 +227,7 @@ public void WeakEventManager_AddEventHandler_WhitespaceEventName() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManager_RemoveEventHandler_NullHandler() { // Arrange @@ -234,7 +240,7 @@ public void WeakEventManager_RemoveEventHandler_NullHandler() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManager_RemoveEventHandler_NullEventName() { // Arrange @@ -247,7 +253,7 @@ public void WeakEventManager_RemoveEventHandler_NullEventName() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManager_RemoveEventHandler_EmptyEventName() { // Arrange @@ -260,7 +266,7 @@ public void WeakEventManager_RemoveEventHandler_EmptyEventName() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManager_RemoveEventHandler_WhiteSpaceEventName() { // Arrange @@ -273,7 +279,7 @@ public void WeakEventManager_RemoveEventHandler_WhiteSpaceEventName() #pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManager_HandleEvent_InvalidHandleEvent() { // Arrange diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_ActionT_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_ActionT_Tests.cs index ddc61c429..16f5e2d7d 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_ActionT_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_ActionT_Tests.cs @@ -1,7 +1,7 @@ using System; using Xamarin.CommunityToolkit.Exceptions; using Xamarin.CommunityToolkit.Helpers; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Helpers.WeakEventManagerTests { @@ -15,7 +15,7 @@ public class WeakEventManager_ActionT_Tests : BaseWeakEventManagerTests remove => actionEventManager.RemoveEventHandler(value); } - [Fact] + [Test] public void WeakEventManagerActionT_HandleEvent_ValidImplementation() { // Arrange @@ -24,8 +24,8 @@ public void WeakEventManagerActionT_HandleEvent_ValidImplementation() void HandleDelegateTest(string message) { - Assert.NotNull(message); - Assert.NotEmpty(message); + Assert.IsNotNull(message); + Assert.IsNotEmpty(message); didEventFire = true; ActionEvent -= HandleDelegateTest; @@ -35,10 +35,10 @@ void HandleDelegateTest(string message) actionEventManager.RaiseEvent("Test", nameof(ActionEvent)); // Assert - Assert.True(didEventFire); + Assert.IsTrue(didEventFire); } - [Fact] + [Test] public void WeakEventManagerActionT_HandleEvent_InvalidHandleEventEventName() { // Arrange @@ -47,8 +47,8 @@ public void WeakEventManagerActionT_HandleEvent_InvalidHandleEventEventName() void HandleDelegateTest(string message) { - Assert.NotNull(message); - Assert.NotEmpty(message); + Assert.IsNotNull(message); + Assert.IsNotEmpty(message); didEventFire = true; } @@ -61,7 +61,7 @@ void HandleDelegateTest(string message) ActionEvent -= HandleDelegateTest; } - [Fact] + [Test] public void WeakEventManagerActionT_UnassignedEvent() { // Arrange @@ -71,8 +71,8 @@ public void WeakEventManagerActionT_UnassignedEvent() ActionEvent -= HandleDelegateTest; void HandleDelegateTest(string message) { - Assert.NotNull(message); - Assert.NotEmpty(message); + Assert.IsNotNull(message); + Assert.IsNotEmpty(message); didEventFire = true; } @@ -84,7 +84,7 @@ void HandleDelegateTest(string message) Assert.False(didEventFire); } - [Fact] + [Test] public void WeakEventManagerActionT_UnassignedEventManager() { // Arrange @@ -94,8 +94,8 @@ public void WeakEventManagerActionT_UnassignedEventManager() ActionEvent += HandleDelegateTest; void HandleDelegateTest(string message) { - Assert.NotNull(message); - Assert.NotEmpty(message); + Assert.IsNotNull(message); + Assert.IsNotEmpty(message); didEventFire = true; } @@ -108,7 +108,7 @@ void HandleDelegateTest(string message) ActionEvent -= HandleDelegateTest; } - [Fact] + [Test] public void WeakEventManagerActionT_HandleEvent_InvalidHandleEvent() { // Arrange @@ -117,8 +117,8 @@ public void WeakEventManagerActionT_HandleEvent_InvalidHandleEvent() void HandleDelegateTest(string message) { - Assert.NotNull(message); - Assert.NotEmpty(message); + Assert.IsNotNull(message); + Assert.IsNotEmpty(message); didEventFire = true; } @@ -131,22 +131,22 @@ void HandleDelegateTest(string message) ActionEvent -= HandleDelegateTest; } - [Fact] + [Test] public void WeakEventManagerActionT_AddEventHandler_NullHandler() { // Arrange - Action nullAction = null; + Action? nullAction = null; // Act // Assert -#pragma warning disable CS8604 //Possible null reference argument for parameter +#pragma warning disable CS8604 // Possible null reference argument. Assert.Throws(() => actionEventManager.AddEventHandler(nullAction, nameof(ActionEvent))); -#pragma warning restore CS8604 //Possible null reference argument for parameter +#pragma warning restore CS8604 // Possible null reference argument. } - [Fact] + [Test] public void WeakEventManagerActionT_AddEventHandler_NullEventName() { // Arrange @@ -154,26 +154,26 @@ public void WeakEventManagerActionT_AddEventHandler_NullEventName() // Act // Assert -#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. Assert.Throws(() => actionEventManager.AddEventHandler(s => { var temp = s; }, null)); -#pragma warning restore CS8625 +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Fact] + [Test] public void WeakEventManagerActionT_AddEventHandler_EmptyEventName() { // Arrange - Action nullAction = null; + Action? nullAction = null; // Act // Assert -#pragma warning disable CS8604 //Possible null reference argument for parameter +#pragma warning disable CS8604 // Possible null reference argument. Assert.Throws(() => actionEventManager.AddEventHandler(nullAction, string.Empty)); -#pragma warning restore CS8604 //Possible null reference argument for parameter +#pragma warning restore CS8604 // Possible null reference argument. } - [Fact] + [Test] public void WeakEventManagerActionT_AddEventHandler_WhitespaceEventName() { // Arrange @@ -181,26 +181,24 @@ public void WeakEventManagerActionT_AddEventHandler_WhitespaceEventName() // Act // Assert -#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type Assert.Throws(() => actionEventManager.AddEventHandler(s => { var temp = s; }, " ")); -#pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManagerActionT_RemoveEventHandler_NullHandler() { // Arrange - Action nullAction = null; + Action? nullAction = null; // Act // Assert -#pragma warning disable CS8604 //Possible null reference argument for parameter +#pragma warning disable CS8604 // Possible null reference argument. Assert.Throws(() => actionEventManager.RemoveEventHandler(nullAction)); -#pragma warning restore CS8604 +#pragma warning restore CS8604 // Possible null reference argument. } - [Fact] + [Test] public void WeakEventManagerActionT_RemoveEventHandler_NullEventName() { // Arrange @@ -208,12 +206,12 @@ public void WeakEventManagerActionT_RemoveEventHandler_NullEventName() // Act // Assert -#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. Assert.Throws(() => actionEventManager.RemoveEventHandler(s => { var temp = s; }, null)); -#pragma warning restore CS8625 +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Fact] + [Test] public void WeakEventManagerActionT_RemoveEventHandler_EmptyEventName() { // Arrange @@ -221,12 +219,10 @@ public void WeakEventManagerActionT_RemoveEventHandler_EmptyEventName() // Act // Assert -#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type Assert.Throws(() => actionEventManager.RemoveEventHandler(s => { var temp = s; }, string.Empty)); -#pragma warning restore CS8625 } - [Fact] + [Test] public void WeakEventManagerActionT_RemoveEventHandler_WhiteSpaceEventName() { // Arrange @@ -234,9 +230,7 @@ public void WeakEventManagerActionT_RemoveEventHandler_WhiteSpaceEventName() // Act // Assert -#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type Assert.Throws(() => actionEventManager.RemoveEventHandler(s => { var temp = s; }, " ")); -#pragma warning restore CS8625 } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_EventHandlerT_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_EventHandlerT_Tests.cs index 8d5190f7f..8aefbae3b 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_EventHandlerT_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Helpers/WeakEventManagerTests/WeakEventManager_EventHandlerT_Tests.cs @@ -2,13 +2,13 @@ using Xamarin.CommunityToolkit.Exceptions; using Xamarin.CommunityToolkit.Helpers; using Xamarin.Forms; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Helpers.WeakEventManagerTests { public class WeakEventManager_EventHandlerT_Tests : BaseWeakEventManagerTests { - [Fact] + [Test] public void WeakEventManagerTEventArgs_HandleEvent_ValidImplementation() { // Arrange @@ -17,16 +17,16 @@ public void WeakEventManagerTEventArgs_HandleEvent_ValidImplementation() const string stringEventArg = "Test"; var didEventFire = false; - void HandleTestEvent(object sender, string? e) + void HandleTestEvent(object? sender, string? e) { if (sender == null || e == null) throw new ArgumentNullException(nameof(sender)); - Assert.NotNull(sender); - Assert.Equal(GetType(), sender.GetType()); + Assert.IsNotNull(sender); + Assert.AreEqual(GetType(), sender.GetType()); - Assert.NotNull(e); - Assert.Equal(stringEventArg, e); + Assert.IsNotNull(e); + Assert.AreEqual(stringEventArg, e); didEventFire = true; TestStringEvent -= HandleTestEvent; @@ -36,10 +36,10 @@ void HandleTestEvent(object sender, string? e) TestStringWeakEventManager.RaiseEvent(this, stringEventArg, nameof(TestStringEvent)); // Assert - Assert.True(didEventFire); + Assert.IsTrue(didEventFire); } - [Fact] + [Test] public void WeakEventManageTEventArgs_HandleEvent_NullSender() { // Arrange @@ -49,38 +49,40 @@ public void WeakEventManageTEventArgs_HandleEvent_NullSender() var didEventFire = false; - void HandleTestEvent(object sender, string e) + void HandleTestEvent(object? sender, string e) { Assert.Null(sender); - Assert.NotNull(e); - Assert.Equal(stringEventArg, e); + Assert.IsNotNull(e); + Assert.AreEqual(stringEventArg, e); didEventFire = true; TestStringEvent -= HandleTestEvent; } // Act +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. TestStringWeakEventManager.RaiseEvent(null, stringEventArg, nameof(TestStringEvent)); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. // Assert - Assert.True(didEventFire); + Assert.IsTrue(didEventFire); } - [Fact] + [Test] public void WeakEventManagerTEventArgs_HandleEvent_NullEventArgs() { // Arrange TestStringEvent += HandleTestEvent; var didEventFire = false; - void HandleTestEvent(object sender, string e) + void HandleTestEvent(object? sender, string e) { if (sender == null) throw new ArgumentNullException(nameof(sender)); - Assert.NotNull(sender); - Assert.Equal(GetType(), sender.GetType()); + Assert.IsNotNull(sender); + Assert.AreEqual(GetType(), sender.GetType()); Assert.Null(e); @@ -94,10 +96,10 @@ void HandleTestEvent(object sender, string e) #pragma warning restore CS8625 // Assert - Assert.True(didEventFire); + Assert.IsTrue(didEventFire); } - [Fact] + [Test] public void WeakEventManagerTEventArgs_HandleEvent_InvalidHandleEvent() { // Arrange @@ -105,7 +107,7 @@ public void WeakEventManagerTEventArgs_HandleEvent_InvalidHandleEvent() var didEventFire = false; - void HandleTestEvent(object sender, string e) => didEventFire = true; + void HandleTestEvent(object? sender, string e) => didEventFire = true; // Act TestStringWeakEventManager.RaiseEvent(this, "Test", nameof(TestEvent)); @@ -115,21 +117,21 @@ public void WeakEventManagerTEventArgs_HandleEvent_InvalidHandleEvent() TestStringEvent -= HandleTestEvent; } - [Fact] + [Test] public void WeakEventManager_NullEventManager() { // Arrange - WeakEventManager unassignedEventManager = null; + WeakEventManager? unassignedEventManager = null; // Act // Assert #pragma warning disable CS8602 //Dereference of a possible null reference - Assert.Throws(() => unassignedEventManager.RaiseEvent(null, null, nameof(TestEvent))); + Assert.Throws(() => unassignedEventManager.RaiseEvent(this, string.Empty, nameof(TestEvent))); #pragma warning restore CS8602 } - [Fact] + [Test] public void WeakEventManagerTEventArgs_UnassignedEventManager() { // Arrange @@ -137,7 +139,7 @@ public void WeakEventManagerTEventArgs_UnassignedEventManager() var didEventFire = false; TestStringEvent += HandleTestEvent; - void HandleTestEvent(object sender, string e) => didEventFire = true; + void HandleTestEvent(object? sender, string e) => didEventFire = true; // Act #pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type @@ -149,7 +151,7 @@ public void WeakEventManagerTEventArgs_UnassignedEventManager() TestStringEvent -= HandleTestEvent; } - [Fact] + [Test] public void WeakEventManagerTEventArgs_UnassignedEvent() { // Arrange @@ -157,7 +159,7 @@ public void WeakEventManagerTEventArgs_UnassignedEvent() TestStringEvent += HandleTestEvent; TestStringEvent -= HandleTestEvent; - void HandleTestEvent(object sender, string e) => didEventFire = true; + void HandleTestEvent(object? sender, string e) => didEventFire = true; // Act TestStringWeakEventManager.RaiseEvent(this, "Test", nameof(TestStringEvent)); @@ -166,7 +168,7 @@ public void WeakEventManagerTEventArgs_UnassignedEvent() Assert.False(didEventFire); } - [Fact] + [Test] public void WeakEventManagerT_AddEventHandler_NullHandler() { // Arrange @@ -174,12 +176,12 @@ public void WeakEventManagerT_AddEventHandler_NullHandler() // Act // Assert -#pragma warning disable CS8625 //Cannot convert null literal to non-nullable reference type - Assert.Throws(() => TestStringWeakEventManager.AddEventHandler((EventHandler)null)); -#pragma warning restore CS8625 +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + Assert.Throws(() => TestStringWeakEventManager.AddEventHandler((EventHandler?)null)); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Fact] + [Test] public void WeakEventManagerT_AddEventHandler_NullEventName() { // Arrange @@ -187,12 +189,12 @@ public void WeakEventManagerT_AddEventHandler_NullEventName() // Act // Assert -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, null)); -#pragma warning restore CS8625 +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Fact] + [Test] public void WeakEventManagerT_AddEventHandler_EmptyEventName() { // Arrange @@ -203,7 +205,7 @@ public void WeakEventManagerT_AddEventHandler_EmptyEventName() Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, string.Empty)); } - [Fact] + [Test] public void WeakEventManagerT_AddEventHandler_WhiteSpaceEventName() { // Arrange @@ -214,7 +216,7 @@ public void WeakEventManagerT_AddEventHandler_WhiteSpaceEventName() Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, " ")); } - [Fact] + [Test] public void WeakEventManagerT_RemoveEventHandler_NullHandler() { // Arrange @@ -222,12 +224,12 @@ public void WeakEventManagerT_RemoveEventHandler_NullHandler() // Act // Assert -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference - Assert.Throws(() => TestStringWeakEventManager.RemoveEventHandler((EventHandler)null)); -#pragma warning restore CS8625 +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + Assert.Throws(() => TestStringWeakEventManager.RemoveEventHandler((EventHandler?)null)); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Fact] + [Test] public void WeakEventManagerT_RemoveEventHandler_NullEventName() { // Arrange @@ -235,12 +237,12 @@ public void WeakEventManagerT_RemoveEventHandler_NullEventName() // Act // Assert -#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, null)); -#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. } - [Fact] + [Test] public void WeakEventManagerT_RemoveEventHandler_EmptyEventName() { // Arrange @@ -251,7 +253,7 @@ public void WeakEventManagerT_RemoveEventHandler_EmptyEventName() Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, string.Empty)); } - [Fact] + [Test] public void WeakEventManagerT_RemoveEventHandler_WhiteSpaceEventName() { // Arrange @@ -262,19 +264,19 @@ public void WeakEventManagerT_RemoveEventHandler_WhiteSpaceEventName() Assert.Throws(() => TestStringWeakEventManager.AddEventHandler(s => { var temp = s; }, string.Empty)); } - [Fact] + [Test] public void WeakEventManagerT_HandleEvent_InvalidHandleEvent() { // Arrange TestStringEvent += HandleTestStringEvent; var didEventFire = false; - void HandleTestStringEvent(object sender, string e) => didEventFire = true; + void HandleTestStringEvent(object? sender, string e) => didEventFire = true; // Act // Assert - Assert.Throws(() => TestStringWeakEventManager.RaiseEvent("", nameof(TestStringEvent))); + Assert.Throws(() => TestStringWeakEventManager.RaiseEvent(string.Empty, nameof(TestStringEvent))); Assert.False(didEventFire); TestStringEvent -= HandleTestStringEvent; } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Mocks/MockEnum.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Mocks/MockEnum.cs new file mode 100644 index 000000000..6ceae3df9 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Mocks/MockEnum.cs @@ -0,0 +1,9 @@ +namespace Xamarin.CommunityToolkit.UnitTests.Mocks +{ + public enum MockEnum + { + Foo, + Bar, + Baz + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Mocks/MockItem.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Mocks/MockItem.cs new file mode 100644 index 000000000..df12403ec --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Mocks/MockItem.cs @@ -0,0 +1,12 @@ +namespace Xamarin.CommunityToolkit.UnitTests.Mocks +{ + public class MockItem + { + public string? Title { get; set; } + + public bool Completed { get; set; } + + public override string ToString() => Completed ? + $"{Title} is completed" : $"{Title} has yet to be completed"; + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Mocks/MockPlatformServices.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Mocks/MockPlatformServices.cs index 18d3fcc03..ec9756930 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Mocks/MockPlatformServices.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Mocks/MockPlatformServices.cs @@ -10,34 +10,27 @@ namespace Xamarin.CommunityToolkit.UnitTests.Mocks { class MockPlatformServices : IPlatformServices { - public string GetHash(string input) - => string.Empty; + public string GetHash(string input) => string.Empty; - public string GetMD5Hash(string input) - => string.Empty; + public string GetMD5Hash(string input) => string.Empty; - public double GetNamedSize(NamedSize size, Type targetElement, bool useOldSizes) - => 0; + public double GetNamedSize(NamedSize size, Type targetElement, bool useOldSizes) => 0; - public Color GetNamedColor(string name) - => Color.Default; + public Color GetNamedColor(string name) => Color.Default; public void OpenUriAction(Uri uri) { } - public bool IsInvokeRequired - => false; + public bool IsInvokeRequired { get; } = false; - public OSAppTheme RequestedTheme => OSAppTheme.Unspecified; + public OSAppTheme RequestedTheme { get; } = OSAppTheme.Unspecified; - public string RuntimePlatform { get; set; } + public string RuntimePlatform { get; set; } = string.Empty; - public void BeginInvokeOnMainThread(Action action) - => action(); + public void BeginInvokeOnMainThread(Action action) => action(); - public Ticker CreateTicker() - => new MockTicker(); + public Ticker CreateTicker() => new MockTicker(); public void StartTimer(TimeSpan interval, Func callback) { @@ -46,21 +39,17 @@ public void StartTimer(TimeSpan interval, Func callback) public Task GetStreamAsync(Uri uri, CancellationToken cancellationToken) => Task.FromResult(new MemoryStream()); - public Assembly[] GetAssemblies() - => new Assembly[0]; + public Assembly[] GetAssemblies() => Array.Empty(); - public IIsolatedStorageFile GetUserStoreForApplication() - => null; + public IIsolatedStorageFile? GetUserStoreForApplication() => null; - Assembly[] IPlatformServices.GetAssemblies() - => new Assembly[0]; + Assembly[] IPlatformServices.GetAssemblies() => Array.Empty(); public void QuitApplication() { } - public SizeRequest GetNativeSize(VisualElement view, double widthConstraint, double heightConstraint) - => default; + public SizeRequest GetNativeSize(VisualElement view, double widthConstraint, double heightConstraint) => default; } class MockTicker : Ticker diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Mocks/MockResourceManager.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Mocks/MockResourceManager.cs index c57aa3b39..3c0289d2a 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Mocks/MockResourceManager.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Mocks/MockResourceManager.cs @@ -5,6 +5,6 @@ namespace Xamarin.CommunityToolkit.UnitTests.Mocks { class MockResourceManager : ResourceManager { - public override string GetString(string name, CultureInfo culture) => culture.EnglishName; + public override string GetString(string name, CultureInfo? culture) => culture?.EnglishName ?? string.Empty; } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Namespace_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Namespace_Tests.cs index 3f710be90..a0c2845f0 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Namespace_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Namespace_Tests.cs @@ -1,76 +1,77 @@ -using System.Linq; +using System; +using System.Linq; using System.Reflection; +using NUnit.Framework; using Xamarin.CommunityToolkit.Behaviors.Internals; using Xamarin.CommunityToolkit.Converters; using Xamarin.CommunityToolkit.Effects; using Xamarin.CommunityToolkit.Extensions; using Xamarin.CommunityToolkit.UI.Views; using Xamarin.Forms; -using Xunit; namespace Xamarin.CommunityToolkit.UnitTests { public class Namespace_Tests { - [Fact] + [Test] public void MakeSureConvertersAreInTheRightNamespace() { - var allTheTypes = Assembly.GetAssembly(typeof(InvertedBoolConverter)).GetTypes(); + var allTheTypes = Assembly.GetAssembly(typeof(InvertedBoolConverter))?.GetTypes() ?? throw new NullReferenceException(); foreach (var type in allTheTypes.Where(t => t.Name.EndsWith("Converter") && t.GetInterface(nameof(IValueConverter)) != null)) { - Assert.True(type.Namespace.Equals("Xamarin.CommunityToolkit.Converters"), - $"{type.FullName} not in Xamarin.CommunityToolkit.Converters namespace"); + Assert.IsTrue(type?.Namespace?.Equals("Xamarin.CommunityToolkit.Converters"), + $"{type?.FullName} not in Xamarin.CommunityToolkit.Converters namespace"); } } - [Fact] + [Test] public void MakeSureEffectsAreInTheRightNamespace() { - var allTheTypes = Assembly.GetAssembly(typeof(SafeAreaEffect)).GetTypes(); + var allTheTypes = Assembly.GetAssembly(typeof(SafeAreaEffect))?.GetTypes() ?? throw new NullReferenceException(); foreach (var type in allTheTypes.Where(t => t.Name.EndsWith("Effect") && t.IsClass && t.IsSealed && t.IsAbstract)) { - Assert.True(type.Namespace.Equals("Xamarin.CommunityToolkit.Effects"), - $"{type.FullName} not in Xamarin.CommunityToolkit.Effects namespace"); + Assert.IsTrue(type?.Namespace?.Equals("Xamarin.CommunityToolkit.Effects"), + $"{type?.FullName} not in Xamarin.CommunityToolkit.Effects namespace"); } } - [Fact] + [Test] public void MakeSureMarkupExtensionsAreInTheRightNamespace() { - var allTheTypes = Assembly.GetAssembly(typeof(TranslateExtension)).GetTypes(); + var allTheTypes = Assembly.GetAssembly(typeof(TranslateExtension))?.GetTypes() ?? throw new NullReferenceException(); foreach (var type in allTheTypes.Where(t => t.Name.EndsWith("Extension") && t.GetInterface("IMarkupExtension") != null)) { - Assert.True(type.Namespace.Equals("Xamarin.CommunityToolkit.Extensions") || - type.Namespace.Equals("Xamarin.CommunityToolkit.Extensions.Internals"), - $"{type.FullName} not in nameof(Xamarin.CommunityToolkit.Extensions namespace"); + Assert.IsTrue(type?.Namespace?.Equals("Xamarin.CommunityToolkit.Extensions") is true || + type?.Namespace?.Equals("Xamarin.CommunityToolkit.Extensions.Internals") is true, + $"{type?.FullName} not in nameof(Xamarin.CommunityToolkit.Extensions namespace"); } } - [Fact] + [Test] public void MakeSureBehaviorsAreInTheRightNamespace() { - var allTheTypes = Assembly.GetAssembly(typeof(BaseBehavior<>)).GetTypes(); + var allTheTypes = Assembly.GetAssembly(typeof(BaseBehavior<>))?.GetTypes() ?? throw new NullReferenceException(); foreach (var type in allTheTypes.Where(t => t.Name.EndsWith("Behavior") && t.IsSubclassOf(typeof(BaseBehavior<>)))) { - Assert.True(type.Namespace.Equals("Xamarin.CommunityToolkit.Behaviors"), - $"{type.FullName} not in Xamarin.CommunityToolkit.Behaviors namespace"); + Assert.IsTrue(type?.Namespace?.Equals("Xamarin.CommunityToolkit.Behaviors"), + $"{type?.FullName} not in Xamarin.CommunityToolkit.Behaviors namespace"); } } - [Fact] + [Test] public void MakeSureViewsAreInTheRightNamespace() { - var allTheTypes = Assembly.GetAssembly(typeof(AvatarView)).GetTypes(); + var allTheTypes = Assembly.GetAssembly(typeof(AvatarView))?.GetTypes() ?? throw new NullReferenceException(); foreach (var type in allTheTypes.Where(t => t.IsSubclassOf(typeof(View)))) { - Assert.True(type.Namespace.Equals("Xamarin.CommunityToolkit.UI.Views") || - type.Namespace.Equals("Xamarin.CommunityToolkit.UI.Views.Internals"), - $"{type.FullName} not in Xamarin.CommunityToolkit.UI.Views namespace"); + Assert.IsTrue(type?.Namespace?.Equals("Xamarin.CommunityToolkit.UI.Views") is true || + type?.Namespace?.Equals("Xamarin.CommunityToolkit.UI.Views.Internals") is true, + $"{type?.FullName} not in Xamarin.CommunityToolkit.UI.Views namespace"); } } } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/Grouping_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/Grouping_Tests.cs new file mode 100644 index 000000000..b7f492032 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/Grouping_Tests.cs @@ -0,0 +1,57 @@ +using System.Linq; +using Xamarin.CommunityToolkit.ObjectModel; +using NUnit.Framework; + +namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel +{ + public sealed class Grouping_Tests + { + readonly Person[] people = new[] + { + new Person { FirstName = "Joseph", LastName = "Hill" }, + new Person { FirstName = "James", LastName = "Montemagno" }, + new Person { FirstName = "Pierce", LastName = "Boggan" }, + }; + + [Test] + public void Grouping() + { + var sorted = from person in people + orderby person.FirstName + group person by person.Group + into personGroup + select new Grouping(personGroup.Key, personGroup); + + var grouped = new ObservableRangeCollection>(); + grouped.AddRange(sorted); + + Assert.AreEqual(2, grouped.Count); + Assert.AreEqual("J", grouped[0].Key); + Assert.AreEqual(2, grouped[0].Count); + Assert.AreEqual(1, grouped[1].Count); + Assert.AreEqual(2, grouped[0].Items.Count); + Assert.AreEqual(1, grouped[1].Items.Count); + } + + [Test] + public void GroupingSubKey() + { + var sorted = from person in people + orderby person.FirstName + group person by person.Group + into personGroup + select new Grouping(personGroup.Key, personGroup.Key, personGroup); + + var grouped = new ObservableRangeCollection>(); + grouped.AddRange(sorted); + + Assert.AreEqual(2, grouped.Count); + Assert.AreEqual("J", grouped[0].SubKey); + Assert.AreEqual("J", grouped[0].Key); + Assert.AreEqual(2, grouped[0].Count); + Assert.AreEqual(1, grouped[1].Count); + Assert.AreEqual(2, grouped[0].Items.Count); + Assert.AreEqual(1, grouped[1].Items.Count); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/AsyncCommand_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/AsyncCommand_Tests.cs index 63cbe4e0c..1abf42f5e 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/AsyncCommand_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/AsyncCommand_Tests.cs @@ -1,13 +1,14 @@ using System; +using System.Threading; using System.Threading.Tasks; using Xamarin.CommunityToolkit.ObjectModel; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.AsyncCommandTests { public class AsyncCommandTests : BaseAsyncCommandTests { - [Fact] + [Test] public void AsyncCommand_NullExecuteParameter() { // Arrange @@ -22,35 +23,33 @@ public void AsyncCommand_NullExecuteParameter() #pragma warning restore CS8625 } - [Theory] - [InlineData(500)] - [InlineData(0)] + [TestCase(500)] + [TestCase(0)] public async Task AsyncCommand_ExecuteAsync_IntParameter_Test(int parameter) { // Arrange var command = new AsyncCommand(IntParameterTask); // Act - await command.ExecuteAsync(parameter); + await command.ExecuteAsync(parameter).ConfigureAwait(false); // Assert } - [Theory] - [InlineData("Hello")] - [InlineData(default)] + [TestCase("Hello")] + [TestCase(default)] public async Task AsyncCommand_ExecuteAsync_StringParameter_Test(string parameter) { // Arrange var command = new AsyncCommand(StringParameterTask); // Act - await command.ExecuteAsync(parameter); + await command.ExecuteAsync(parameter).ConfigureAwait(false); // Assert } - [Fact] + [Test] public void AsyncCommand_Parameter_CanExecuteTrue_Test() { // Arrange @@ -60,10 +59,10 @@ public void AsyncCommand_Parameter_CanExecuteTrue_Test() // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); } - [Fact] + [Test] public void AsyncCommand_Parameter_CanExecuteFalse_Test() { // Arrange @@ -75,7 +74,7 @@ public void AsyncCommand_Parameter_CanExecuteFalse_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] public void AsyncCommand_NoParameter_CanExecuteTrue_Test() { // Arrange @@ -84,10 +83,10 @@ public void AsyncCommand_NoParameter_CanExecuteTrue_Test() // Act // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); } - [Fact] + [Test] public void AsyncCommand_NoParameter_CanExecuteFalse_Test() { // Arrange @@ -99,7 +98,7 @@ public void AsyncCommand_NoParameter_CanExecuteFalse_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] public void AsyncCommand_Parameter_CanExecuteTrue_NoParameter_Test() { // Arrange @@ -109,10 +108,10 @@ public void AsyncCommand_Parameter_CanExecuteTrue_NoParameter_Test() // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); } - [Fact] + [Test] public void AsyncCommand_Parameter_CanExecuteFalse_NoParameter_Test() { // Arrange @@ -124,7 +123,7 @@ public void AsyncCommand_Parameter_CanExecuteFalse_NoParameter_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] public void AsyncCommand_NoParameter_CanExecuteTrue_NoParameter_Test() { // Arrange @@ -133,10 +132,10 @@ public void AsyncCommand_NoParameter_CanExecuteTrue_NoParameter_Test() // Act // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); } - [Fact] + [Test] public void AsyncCommand_NoParameter_CanExecuteFalse_NoParameter_Test() { // Arrange @@ -148,30 +147,36 @@ public void AsyncCommand_NoParameter_CanExecuteFalse_NoParameter_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] public void AsyncCommand_NoParameter_NoCanExecute_Test() { // Arrange - Func canExecute = null; + Func? canExecute = null; +#pragma warning disable CS8604 // Possible null reference argument. var command = new AsyncCommand(NoParameterTask, canExecute); +#pragma warning restore CS8604 // Possible null reference argument. // Act // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); } - [Fact] - public void AsyncCommand_RaiseCanExecuteChanged_MainThreadCreation_MainThreadExecution_Test() + [Test] + [Timeout(ICommandTestTimeout)] + public async Task AsyncCommand_RaiseCanExecuteChanged_MainThreadCreation_MainThreadExecution_Test() { // Arrange + var semaphoreSlim = new SemaphoreSlim(1, 1); var canCommandExecute = false; var didCanExecuteChangeFire = false; + var handleCanExecuteChangedTCS = new TaskCompletionSource(); + var command = new AsyncCommand(NoParameterTask, commandCanExecute); command.CanExecuteChanged += handleCanExecuteChanged; - bool commandCanExecute(object parameter) => canCommandExecute; + bool commandCanExecute(object? parameter) => canCommandExecute; Assert.False(command.CanExecute(null)); @@ -179,32 +184,54 @@ public void AsyncCommand_RaiseCanExecuteChanged_MainThreadCreation_MainThreadExe canCommandExecute = true; // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); Assert.False(didCanExecuteChangeFire); // Act command.RaiseCanExecuteChanged(); + await handleCanExecuteChangedTCS.Task.ConfigureAwait(false); // Assert - Assert.True(didCanExecuteChangeFire); - Assert.True(command.CanExecute(null)); + Assert.IsTrue(didCanExecuteChangeFire); + Assert.IsTrue(command.CanExecute(null)); - void handleCanExecuteChanged(object sender, EventArgs e) => didCanExecuteChangeFire = true; + async void handleCanExecuteChanged(object? sender, EventArgs e) + { + await semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try + { + command.CanExecuteChanged -= handleCanExecuteChanged; + didCanExecuteChangeFire = true; + handleCanExecuteChangedTCS.SetResult(null); + } + finally + { + semaphoreSlim.Release(); + } + } } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public Task AsyncCommand_RaiseCanExecuteChanged_BackgroundThreadCreation_BackgroundThreadExecution_Test() => Task.Run(async () => { // Arrange + + // Use ConfigureAwait(false) to move to a background thread await Task.Delay(100).ConfigureAwait(false); + var semaphoreSlim = new SemaphoreSlim(1, 1); + + var handleCanExecuteChangedTCS = new TaskCompletionSource(); + var canCommandExecute = false; var didCanExecuteChangeFire = false; var command = new AsyncCommand(NoParameterTask, commandCanExecute); command.CanExecuteChanged += handleCanExecuteChanged; - bool commandCanExecute(object parameter) => canCommandExecute; + bool commandCanExecute(object? parameter) => canCommandExecute; Assert.False(command.CanExecute(null)); @@ -212,23 +239,41 @@ public void AsyncCommand_RaiseCanExecuteChanged_MainThreadCreation_MainThreadExe canCommandExecute = true; // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); Assert.False(didCanExecuteChangeFire); // Act command.RaiseCanExecuteChanged(); + await handleCanExecuteChangedTCS.Task; // Assert - Assert.True(didCanExecuteChangeFire); - Assert.True(command.CanExecute(null)); + Assert.IsTrue(didCanExecuteChangeFire); + Assert.IsTrue(command.CanExecute(null)); - void handleCanExecuteChanged(object sender, EventArgs e) => didCanExecuteChangeFire = true; + async void handleCanExecuteChanged(object? sender, EventArgs e) + { + await semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try + { + command.CanExecuteChanged -= handleCanExecuteChanged; + didCanExecuteChangeFire = true; + handleCanExecuteChangedTCS.SetResult(null); + } + finally + { + semaphoreSlim.Release(); + } + } }); - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task AsyncCommand_RaiseCanExecuteChanged_MainThreadCreation_BackgroundThreadExecution_Test() { // Arrange + var semaphoreSlim = new SemaphoreSlim(1, 1); + var handleCanExecuteChangedTCS = new TaskCompletionSource(); var canCommandExecute = false; var didCanExecuteChangeFire = false; @@ -236,7 +281,7 @@ public async Task AsyncCommand_RaiseCanExecuteChanged_MainThreadCreation_Backgro var command = new AsyncCommand(NoParameterTask, commandCanExecute); command.CanExecuteChanged += handleCanExecuteChanged; - bool commandCanExecute(object parameter) => canCommandExecute; + bool commandCanExecute(object? parameter) => canCommandExecute; Assert.False(command.CanExecute(null)); @@ -244,31 +289,52 @@ public async Task AsyncCommand_RaiseCanExecuteChanged_MainThreadCreation_Backgro canCommandExecute = true; // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); Assert.False(didCanExecuteChangeFire); // Act await Task.Run(async () => { + // use ConfigureAwait(false) to return to a background thread await Task.Delay(100).ConfigureAwait(false); + command.RaiseCanExecuteChanged(); + await handleCanExecuteChangedTCS.Task.ConfigureAwait(false); // Assert - Assert.True(didCanExecuteChangeFire); - Assert.True(command.CanExecute(null)); - }); + Assert.IsTrue(didCanExecuteChangeFire); + Assert.IsTrue(command.CanExecute(null)); + }).ConfigureAwait(false); - void handleCanExecuteChanged(object sender, EventArgs e) => didCanExecuteChangeFire = true; + async void handleCanExecuteChanged(object? sender, EventArgs e) + { + await semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try + { + command.CanExecuteChanged -= handleCanExecuteChanged; + didCanExecuteChangeFire = true; + handleCanExecuteChangedTCS.SetResult(null); + } + finally + { + semaphoreSlim.Release(); + } + } } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task AsyncCommand_RaiseCanExecuteChanged_BackgroundThreadCreation_MainThreadExecution_Test() { // Arrange - AsyncCommand command = null; + var semaphoreSlim = new SemaphoreSlim(1, 1); + AsyncCommand? command = null; var didCanExecuteChangeFire = false; var canCommandExecute = false; + var handleCanExecuteChangedTCS = new TaskCompletionSource(); + await Task.Run(async () => { await Task.Delay(100).ConfigureAwait(false); @@ -276,7 +342,7 @@ public async Task AsyncCommand_RaiseCanExecuteChanged_BackgroundThreadCreation_M command = new AsyncCommand(NoParameterTask, commandCanExecute); command.CanExecuteChanged += handleCanExecuteChanged; - bool commandCanExecute(object parameter) => canCommandExecute; + bool commandCanExecute(object? parameter) => canCommandExecute; Assert.False(command.CanExecute(null)); @@ -284,25 +350,45 @@ public async Task AsyncCommand_RaiseCanExecuteChanged_BackgroundThreadCreation_M canCommandExecute = true; // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); Assert.False(didCanExecuteChangeFire); }).ConfigureAwait(true); // Act + if (command is null) + throw new NullReferenceException(); + command.RaiseCanExecuteChanged(); + await handleCanExecuteChangedTCS.Task.ConfigureAwait(false); // Assert - Assert.True(didCanExecuteChangeFire); - Assert.True(command.CanExecute(null)); + Assert.IsTrue(didCanExecuteChangeFire); + Assert.IsTrue(command.CanExecute(null)); - void handleCanExecuteChanged(object sender, EventArgs e) => didCanExecuteChangeFire = true; + async void handleCanExecuteChanged(object? sender, EventArgs e) + { + await semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try + { + command.CanExecuteChanged -= handleCanExecuteChanged; + didCanExecuteChangeFire = true; + handleCanExecuteChangedTCS.SetResult(null); + } + finally + { + semaphoreSlim.Release(); + } + } } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task AsyncCommand_ChangeCanExecute_Test() { // Arrange - var canExecuteChangedTCS = new TaskCompletionSource(); + var semaphoreSlim = new SemaphoreSlim(1, 1); + var canExecuteChangedTCS = new TaskCompletionSource(); var canCommandExecute = false; var didCanExecuteChangeFire = false; @@ -310,7 +396,7 @@ public async Task AsyncCommand_ChangeCanExecute_Test() var command = new AsyncCommand(NoParameterTask, commandCanExecute); command.CanExecuteChanged += handleCanExecuteChanged; - bool commandCanExecute(object parameter) => canCommandExecute; + bool commandCanExecute(object? parameter) => canCommandExecute; Assert.False(command.CanExecute(null)); @@ -318,27 +404,36 @@ public async Task AsyncCommand_ChangeCanExecute_Test() canCommandExecute = true; // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); Assert.False(didCanExecuteChangeFire); // Act -#pragma warning disable CS0618 // Type or member is obsolete command.ChangeCanExecute(); - await canExecuteChangedTCS.Task; -#pragma warning restore CS0618 // Type or member is obsolete + await canExecuteChangedTCS.Task.ConfigureAwait(false); // Assert - Assert.True(didCanExecuteChangeFire); - Assert.True(command.CanExecute(null)); + Assert.IsTrue(didCanExecuteChangeFire); + Assert.IsTrue(command.CanExecute(null)); - void handleCanExecuteChanged(object sender, EventArgs e) + async void handleCanExecuteChanged(object? sender, EventArgs e) { - didCanExecuteChangeFire = true; - canExecuteChangedTCS.SetResult(null); + await semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try + { + command.CanExecuteChanged -= handleCanExecuteChanged; + didCanExecuteChangeFire = true; + canExecuteChangedTCS.SetResult(null); + } + finally + { + semaphoreSlim.Release(); + } } } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task AsyncCommand_CanExecuteChanged_AllowsMultipleExecutions_Test() { // Arrange @@ -347,30 +442,34 @@ public async Task AsyncCommand_CanExecuteChanged_AllowsMultipleExecutions_Test() var command = new AsyncCommand(IntParameterTask); command.CanExecuteChanged += handleCanExecuteChanged; - Assert.True(command.AllowsMultipleExecutions); + Assert.IsTrue(command.AllowsMultipleExecutions); // Act var asyncCommandTask = command.ExecuteAsync(Delay); // Assert - Assert.True(command.IsExecuting); - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.IsExecuting); + Assert.IsTrue(command.CanExecute(null)); // Act - await asyncCommandTask; + await asyncCommandTask.ConfigureAwait(false); // Assert - Assert.True(command.CanExecute(null)); - Assert.Equal(0, canExecuteChangedCount); + Assert.IsTrue(command.CanExecute(null)); + Assert.AreEqual(0, canExecuteChangedCount); + + command.CanExecuteChanged -= handleCanExecuteChanged; - void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + void handleCanExecuteChanged(object? sender, EventArgs e) => canExecuteChangedCount++; } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task AsyncCommand_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test() { // Arrange - var canExecuteChangedGreaterThan1TCS = new TaskCompletionSource(); + var semaphoreSlim = new SemaphoreSlim(1, 1); + var canExecuteChangedGreaterThan1TCS = new TaskCompletionSource(); var canExecuteChangedCount = 0; @@ -383,21 +482,34 @@ public async Task AsyncCommand_CanExecuteChanged_DoesNotAllowMultipleExecutions_ var asyncCommandTask = command.ExecuteAsync(Delay); // Assert - Assert.True(command.IsExecuting); + Assert.IsTrue(command.IsExecuting); Assert.False(command.CanExecute(null)); // Act - await asyncCommandTask; - await canExecuteChangedGreaterThan1TCS.Task; + await asyncCommandTask.ConfigureAwait(false); + var canExecuteChangedGreaterThan1Result = await canExecuteChangedGreaterThan1TCS.Task.ConfigureAwait(false); // Assert - Assert.True(command.CanExecute(null)); - Assert.Equal(2, canExecuteChangedCount); + Assert.IsTrue(command.CanExecute(null)); + Assert.AreEqual(2, canExecuteChangedCount); + Assert.AreEqual(canExecuteChangedCount, canExecuteChangedGreaterThan1Result); - void handleCanExecuteChanged(object sender, EventArgs e) + async void handleCanExecuteChanged(object? sender, EventArgs e) { - if (++canExecuteChangedCount > 1) - canExecuteChangedGreaterThan1TCS.SetResult(null); + await semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try + { + if (++canExecuteChangedCount is 2) + { + command.CanExecuteChanged -= handleCanExecuteChanged; + canExecuteChangedGreaterThan1TCS.SetResult(canExecuteChangedCount); + } + } + finally + { + semaphoreSlim.Release(); + } } } } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/IAsyncCommand_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/IAsyncCommand_Tests.cs index 8c7bdc80a..668fb11d1 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/IAsyncCommand_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/IAsyncCommand_Tests.cs @@ -1,14 +1,15 @@ using System; +using System.Threading; using System.Threading.Tasks; using Xamarin.CommunityToolkit.Exceptions; using Xamarin.CommunityToolkit.ObjectModel; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.AsyncCommandTests { public class IAsyncCommandTests : BaseAsyncCommandTests { - [Fact] + [Test] public void IAsyncCommand_CanExecute_InvalidReferenceParameter() { // Arrange @@ -20,7 +21,7 @@ public void IAsyncCommand_CanExecute_InvalidReferenceParameter() Assert.Throws(() => command.CanExecute("Hello World")); } - [Fact] + [Test] public void IAsyncCommand_Execute_InvalidValueTypeParameter() { // Arrange @@ -32,7 +33,7 @@ public void IAsyncCommand_Execute_InvalidValueTypeParameter() Assert.Throws(() => command.Execute(true)); } - [Fact] + [Test] public void IAsyncCommand_Execute_InvalidReferenceParameter() { // Arrange @@ -44,7 +45,7 @@ public void IAsyncCommand_Execute_InvalidReferenceParameter() Assert.Throws(() => command.Execute("Hello World")); } - [Fact] + [Test] public void IAsyncCommand_CanExecute_InvalidValueTypeParameter() { // Arrange @@ -56,9 +57,8 @@ public void IAsyncCommand_CanExecute_InvalidValueTypeParameter() Assert.Throws(() => command.CanExecute(true)); } - [Theory] - [InlineData("Hello")] - [InlineData(default)] + [TestCase("Hello")] + [TestCase(default)] public async Task AsyncCommand_ExecuteAsync_StringParameter_Test(string parameter) { // Arrange @@ -66,13 +66,13 @@ public async Task AsyncCommand_ExecuteAsync_StringParameter_Test(string paramete IAsyncCommand command2 = new AsyncCommand(StringParameterTask); // Act - await command.ExecuteAsync(parameter); - await command2.ExecuteAsync(parameter); + await command.ExecuteAsync(parameter).ConfigureAwait(false); + await command2.ExecuteAsync(parameter).ConfigureAwait(false); // Assert } - [Fact] + [Test] public void IAsyncCommand_Parameter_CanExecuteTrue_Test() { // Arrange @@ -82,11 +82,11 @@ public void IAsyncCommand_Parameter_CanExecuteTrue_Test() // Act // Assert - Assert.True(command.CanExecute(null)); - Assert.True(command2.CanExecute(true)); + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command2.CanExecute(true)); } - [Fact] + [Test] public void IAsyncCommand_Parameter_CanExecuteFalse_Test() { // Arrange @@ -100,7 +100,7 @@ public void IAsyncCommand_Parameter_CanExecuteFalse_Test() Assert.False(command2.CanExecute("Hello World")); } - [Fact] + [Test] public void IAsyncCommand_NoParameter_CanExecuteTrue_Test() { // Arrange @@ -109,10 +109,10 @@ public void IAsyncCommand_NoParameter_CanExecuteTrue_Test() // Act // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); } - [Fact] + [Test] public void IAsyncCommand_NoParameter_CanExecuteFalse_Test() { // Arrange @@ -124,7 +124,7 @@ public void IAsyncCommand_NoParameter_CanExecuteFalse_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] public void IAsyncCommand_Parameter_CanExecuteTrue_NoParameter_Test() { // Arrange @@ -134,11 +134,11 @@ public void IAsyncCommand_Parameter_CanExecuteTrue_NoParameter_Test() // Act // Assert - Assert.True(command.CanExecute(null)); - Assert.True(command2.CanExecute(true)); + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command2.CanExecute(true)); } - [Fact] + [Test] public void IAsyncCommand_Parameter_CanExecuteFalse_NoParameter_Test() { // Arrange @@ -152,7 +152,7 @@ public void IAsyncCommand_Parameter_CanExecuteFalse_NoParameter_Test() Assert.False(command2.CanExecute("Hello World")); } - [Fact] + [Test] public void IAsyncCommand_NoParameter_CanExecuteTrue_NoParameter_Test() { // Arrange @@ -161,10 +161,10 @@ public void IAsyncCommand_NoParameter_CanExecuteTrue_NoParameter_Test() // Act // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); } - [Fact] + [Test] public void IAsyncCommand_NoParameter_CanExecuteFalse_NoParameter_Test() { // Arrange @@ -176,7 +176,8 @@ public void IAsyncCommand_NoParameter_CanExecuteFalse_NoParameter_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task IAsyncCommand_CanExecuteChanged_AllowsMultipleExecutions_Test() { // Arrange @@ -185,30 +186,35 @@ public async Task IAsyncCommand_CanExecuteChanged_AllowsMultipleExecutions_Test( IAsyncCommand command = new AsyncCommand(IntParameterTask); command.CanExecuteChanged += handleCanExecuteChanged; - Assert.True(command.AllowsMultipleExecutions); + Assert.IsTrue(command.AllowsMultipleExecutions); // Act var asyncCommandTask = command.ExecuteAsync(Delay); // Assert - Assert.True(command.IsExecuting); - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.IsExecuting); + Assert.IsTrue(command.CanExecute(null)); // Act - await asyncCommandTask; + await asyncCommandTask.ConfigureAwait(false); // Assert - Assert.True(command.CanExecute(null)); - Assert.Equal(0, canExecuteChangedCount); + Assert.IsTrue(command.CanExecute(null)); + Assert.AreEqual(0, canExecuteChangedCount); - void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + command.CanExecuteChanged -= handleCanExecuteChanged; + + void handleCanExecuteChanged(object? sender, EventArgs e) => canExecuteChangedCount++; } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task IAsyncCommand_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test() { // Arrange + var semaphoreSlim = new SemaphoreSlim(1, 1); var canExecuteChangedCount = 0; + var handleCanExecuteChangedTCS = new TaskCompletionSource(); IAsyncCommand command = new AsyncCommand(IntParameterTask, allowsMultipleExecutions: false); command.CanExecuteChanged += handleCanExecuteChanged; @@ -219,17 +225,35 @@ public async Task IAsyncCommand_CanExecuteChanged_DoesNotAllowMultipleExecutions var asyncCommandTask = command.ExecuteAsync(Delay); // Assert - Assert.True(command.IsExecuting); + Assert.IsTrue(command.IsExecuting); Assert.False(command.CanExecute(null)); // Act - await asyncCommandTask; + await asyncCommandTask.ConfigureAwait(false); + var handleCanExecuteChangedREsult = await handleCanExecuteChangedTCS.Task.ConfigureAwait(false); // Assert - Assert.True(command.CanExecute(null)); - Assert.Equal(2, canExecuteChangedCount); - - void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + Assert.IsTrue(command.CanExecute(null)); + Assert.AreEqual(2, canExecuteChangedCount); + Assert.AreEqual(canExecuteChangedCount, handleCanExecuteChangedREsult); + + async void handleCanExecuteChanged(object? sender, EventArgs e) + { + await semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try + { + if (++canExecuteChangedCount is 2) + { + command.CanExecuteChanged -= handleCanExecuteChanged; + handleCanExecuteChangedTCS.SetResult(canExecuteChangedCount); + } + } + finally + { + semaphoreSlim.Release(); + } + } } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/ICommand_AsyncCommand_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/ICommand_AsyncCommand_Tests.cs index 691eff1fa..bcaa11cbf 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/ICommand_AsyncCommand_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncCommandTests/ICommand_AsyncCommand_Tests.cs @@ -1,17 +1,17 @@ using System; +using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using Xamarin.CommunityToolkit.Exceptions; using Xamarin.CommunityToolkit.ObjectModel; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.AsyncCommandTests { public class ICommand_AsyncCommandTests : BaseAsyncCommandTests { - [Theory] - [InlineData(500)] - [InlineData(0)] + [TestCase(500)] + [TestCase(0)] public async Task ICommand_Execute_IntParameter_Test(int parameter) { // Arrange @@ -19,14 +19,13 @@ public async Task ICommand_Execute_IntParameter_Test(int parameter) // Act command.Execute(parameter); - await NoParameterTask(); + await NoParameterTask().ConfigureAwait(false); // Assert } - [Theory] - [InlineData("Hello")] - [InlineData(default)] + [TestCase("Hello")] + [TestCase(default)] public async Task ICommand_Execute_StringParameter_Test(string parameter) { // Arrange @@ -34,16 +33,16 @@ public async Task ICommand_Execute_StringParameter_Test(string parameter) // Act command.Execute(parameter); - await NoParameterTask(); + await NoParameterTask().ConfigureAwait(false); // Assert } - [Fact] + [Test] public void ICommand_ExecuteAsync_InvalidValueTypeParameter_Test() { // Arrange - InvalidCommandParameterException actualInvalidCommandParameterException = null; + InvalidCommandParameterException? actualInvalidCommandParameterException = null; var expectedInvalidCommandParameterException = new InvalidCommandParameterException(typeof(string), typeof(int)); ICommand command = new AsyncCommand(StringParameterTask); @@ -53,15 +52,15 @@ public void ICommand_ExecuteAsync_InvalidValueTypeParameter_Test() actualInvalidCommandParameterException = Assert.Throws(() => command.Execute(Delay)); // Assert - Assert.NotNull(actualInvalidCommandParameterException); - Assert.Equal(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); + Assert.IsNotNull(actualInvalidCommandParameterException); + Assert.AreEqual(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); } - [Fact] + [Test] public void ICommand_ExecuteAsync_InvalidReferenceTypeParameter_Test() { // Arrange - InvalidCommandParameterException actualInvalidCommandParameterException = null; + InvalidCommandParameterException? actualInvalidCommandParameterException = null; var expectedInvalidCommandParameterException = new InvalidCommandParameterException(typeof(int), typeof(string)); ICommand command = new AsyncCommand(IntParameterTask); @@ -70,15 +69,15 @@ public void ICommand_ExecuteAsync_InvalidReferenceTypeParameter_Test() actualInvalidCommandParameterException = Assert.Throws(() => command.Execute("Hello World")); // Assert - Assert.NotNull(actualInvalidCommandParameterException); - Assert.Equal(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); + Assert.IsNotNull(actualInvalidCommandParameterException); + Assert.AreEqual(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); } - [Fact] + [Test] public void ICommand_ExecuteAsync_ValueTypeParameter_Test() { // Arrange - InvalidCommandParameterException actualInvalidCommandParameterException = null; + InvalidCommandParameterException? actualInvalidCommandParameterException = null; var expectedInvalidCommandParameterException = new InvalidCommandParameterException(typeof(int)); ICommand command = new AsyncCommand(IntParameterTask); @@ -87,11 +86,11 @@ public void ICommand_ExecuteAsync_ValueTypeParameter_Test() actualInvalidCommandParameterException = Assert.Throws(() => command.Execute(null)); // Assert - Assert.NotNull(actualInvalidCommandParameterException); - Assert.Equal(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); + Assert.IsNotNull(actualInvalidCommandParameterException); + Assert.AreEqual(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); } - [Fact] + [Test] public void ICommand_Parameter_CanExecuteTrue_Test() { // Arrange @@ -100,10 +99,10 @@ public void ICommand_Parameter_CanExecuteTrue_Test() // Act // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); } - [Fact] + [Test] public void ICommand_Parameter_CanExecuteFalse_Test() { // Arrange @@ -115,7 +114,7 @@ public void ICommand_Parameter_CanExecuteFalse_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] public void ICommand_NoParameter_CanExecuteFalse_Test() { // Arrange @@ -127,7 +126,7 @@ public void ICommand_NoParameter_CanExecuteFalse_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] public void ICommand_Parameter_CanExecuteTrue_NoParameter_Test() { // Arrange @@ -136,10 +135,10 @@ public void ICommand_Parameter_CanExecuteTrue_NoParameter_Test() // Act // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); } - [Fact] + [Test] public void ICommand_Parameter_CanExecuteFalse_NoParameter_Test() { // Arrange @@ -151,7 +150,7 @@ public void ICommand_Parameter_CanExecuteFalse_NoParameter_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] public void ICommand_NoParameter_CanExecuteFalse_NoParameter_Test() { // Arrange @@ -163,7 +162,7 @@ public void ICommand_NoParameter_CanExecuteFalse_NoParameter_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] public void ICommand_Parameter_CanExecuteDynamic_Test() { // Arrange @@ -172,11 +171,11 @@ public void ICommand_Parameter_CanExecuteDynamic_Test() // Act // Assert - Assert.True(command.CanExecute(true)); + Assert.IsTrue(command.CanExecute(true)); Assert.False(command.CanExecute(false)); } - [Fact] + [Test] public void ICommand_Parameter_CanExecuteChanged_Test() { // Arrange @@ -185,43 +184,49 @@ public void ICommand_Parameter_CanExecuteChanged_Test() // Act // Assert - Assert.True(command.CanExecute(true)); + Assert.IsTrue(command.CanExecute(true)); Assert.False(command.CanExecute(false)); } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task ICommand_Parameter_CanExecuteChanged_AllowsMultipleExecutions_Test() { // Arrange var canExecuteChangedCount = 0; ICommand command = new AsyncCommand(IntParameterTask); + command.CanExecuteChanged += handleCanExecuteChanged; - void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + void handleCanExecuteChanged(object? sender, EventArgs e) => canExecuteChangedCount++; // Act command.Execute(Delay); // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); // Act - await IntParameterTask(Delay); - await IntParameterTask(Delay); + await IntParameterTask(Delay).ConfigureAwait(false); + await IntParameterTask(Delay).ConfigureAwait(false); // Assert - Assert.True(command.CanExecute(null)); - Assert.Equal(0, canExecuteChangedCount); + Assert.IsTrue(command.CanExecute(null)); + Assert.AreEqual(0, canExecuteChangedCount); - command.CanExecuteChanged += handleCanExecuteChanged; + command.CanExecuteChanged -= handleCanExecuteChanged; } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task ICommand_Parameter_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test() { // Arrange + var semaphoreSlim = new SemaphoreSlim(1, 1); var canExecuteChangedCount = 0; + var handleCanExecuteChangedTCS = new TaskCompletionSource(); + ICommand command = new AsyncCommand(IntParameterTask, allowsMultipleExecutions: false); command.CanExecuteChanged += handleCanExecuteChanged; @@ -232,17 +237,34 @@ public async Task ICommand_Parameter_CanExecuteChanged_DoesNotAllowMultipleExecu Assert.False(command.CanExecute(null)); // Act - await IntParameterTask(Delay); - await IntParameterTask(Delay); + var handleCanExecuteChangedTCSResult = await handleCanExecuteChangedTCS.Task.ConfigureAwait(false); // Assert - Assert.True(command.CanExecute(null)); - Assert.Equal(2, canExecuteChangedCount); - - void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + Assert.IsTrue(command.CanExecute(null)); + Assert.AreEqual(2, canExecuteChangedCount); + Assert.AreEqual(canExecuteChangedCount, handleCanExecuteChangedTCSResult); + + async void handleCanExecuteChanged(object? sender, EventArgs e) + { + await semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try + { + if (++canExecuteChangedCount is 2) + { + command.CanExecuteChanged -= handleCanExecuteChanged; + handleCanExecuteChangedTCS.SetResult(canExecuteChangedCount); + } + } + finally + { + semaphoreSlim.Release(); + } + } } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task ICommand_NoParameter_CanExecuteChanged_AllowsMultipleExecutions_Test() { // Arrange @@ -251,33 +273,54 @@ public async Task ICommand_NoParameter_CanExecuteChanged_AllowsMultipleExecution ICommand command = new AsyncCommand(() => IntParameterTask(Delay)); command.CanExecuteChanged += handleCanExecuteChanged; - void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + void handleCanExecuteChanged(object? sender, EventArgs e) => canExecuteChangedCount++; // Act command.Execute(null); // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); // Act - await IntParameterTask(Delay); - await IntParameterTask(Delay); + await IntParameterTask(Delay).ConfigureAwait(false); + await IntParameterTask(Delay).ConfigureAwait(false); // Assert - Assert.True(command.CanExecute(null)); - Assert.Equal(0, canExecuteChangedCount); + Assert.IsTrue(command.CanExecute(null)); + Assert.AreEqual(0, canExecuteChangedCount); + + command.CanExecuteChanged -= handleCanExecuteChanged; } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task ICommand_NoParameter_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test() { // Arrange + var semaphoreSlim = new SemaphoreSlim(1, 1); var canExecuteChangedCount = 0; + var handleCanExecuteChangedTCS = new TaskCompletionSource(); ICommand command = new AsyncCommand(() => IntParameterTask(Delay), allowsMultipleExecutions: false); command.CanExecuteChanged += handleCanExecuteChanged; - void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + async void handleCanExecuteChanged(object? sender, EventArgs e) + { + await semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try + { + if (++canExecuteChangedCount is 2) + { + command.CanExecuteChanged -= handleCanExecuteChanged; + handleCanExecuteChangedTCS.SetResult(canExecuteChangedCount); + } + } + finally + { + semaphoreSlim.Release(); + } + } // Act command.Execute(null); @@ -286,12 +329,12 @@ public async Task ICommand_NoParameter_CanExecuteChanged_DoesNotAllowMultipleExe Assert.False(command.CanExecute(null)); // Act - await IntParameterTask(Delay); - await IntParameterTask(Delay); + var handleCanExecuteChangedREsult = await handleCanExecuteChangedTCS.Task.ConfigureAwait(false); // Assert - Assert.True(command.CanExecute(null)); - Assert.Equal(2, canExecuteChangedCount); + Assert.IsTrue(command.CanExecute(null)); + Assert.AreEqual(2, canExecuteChangedCount); + Assert.AreEqual(canExecuteChangedCount, handleCanExecuteChangedREsult); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/AsyncValueCommand_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/AsyncValueCommand_Tests.cs index 152aed39e..fc8e1b3ef 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/AsyncValueCommand_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/AsyncValueCommand_Tests.cs @@ -1,13 +1,14 @@ using System; +using System.Threading; using System.Threading.Tasks; using Xamarin.CommunityToolkit.ObjectModel; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.AsyncValueCommandTests { public class AsyncValueCommandTests : BaseAsyncValueCommandTests { - [Fact] + [Test] public void AsyncValueCommandNullExecuteParameter() { // Arrange @@ -20,7 +21,7 @@ public void AsyncValueCommandNullExecuteParameter() #pragma warning restore CS8625 } - [Fact] + [Test] public void AsyncValueCommandT_NullExecuteParameter() { // Arrange @@ -33,9 +34,8 @@ public void AsyncValueCommandT_NullExecuteParameter() #pragma warning restore CS8625 } - [Theory] - [InlineData(500)] - [InlineData(0)] + [TestCase(500)] + [TestCase(0)] public async Task AsyncValueCommandExecuteAsync_IntParameter_Test(int parameter) { // Arrange @@ -43,15 +43,14 @@ public async Task AsyncValueCommandExecuteAsync_IntParameter_Test(int parameter) var command2 = new AsyncValueCommand(IntParameterTask, CanExecuteTrue); // Act - await command.ExecuteAsync(parameter); - await command2.ExecuteAsync(parameter); + await command.ExecuteAsync(parameter).ConfigureAwait(false); + await command2.ExecuteAsync(parameter).ConfigureAwait(false); // Assert } - [Theory] - [InlineData("Hello")] - [InlineData(default)] + [TestCase("Hello")] + [TestCase(default)] public async Task AsyncValueCommandExecuteAsync_StringParameter_Test(string parameter) { // Arrange @@ -59,13 +58,13 @@ public async Task AsyncValueCommandExecuteAsync_StringParameter_Test(string para var command2 = new AsyncValueCommand(StringParameterTask, CanExecuteTrue); // Act - await command.ExecuteAsync(parameter); - await command2.ExecuteAsync(parameter); + await command.ExecuteAsync(parameter).ConfigureAwait(false); + await command2.ExecuteAsync(parameter).ConfigureAwait(false); // Assert } - [Fact] + [Test] public void AsyncValueCommandParameter_CanExecuteTrue_Test() { // Arrange @@ -76,11 +75,11 @@ public void AsyncValueCommandParameter_CanExecuteTrue_Test() // Assert - Assert.True(command.CanExecute(null)); - Assert.True(command2.CanExecute(true)); + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command2.CanExecute(true)); } - [Fact] + [Test] public void AsyncValueCommandParameter_CanExecuteFalse_Test() { // Arrange @@ -94,7 +93,7 @@ public void AsyncValueCommandParameter_CanExecuteFalse_Test() Assert.False(command2.CanExecute("Hello World")); } - [Fact] + [Test] public void AsyncValueCommandParameter_CanExecuteTrue_NoParameterTest() { // Arrange @@ -105,11 +104,11 @@ public void AsyncValueCommandParameter_CanExecuteTrue_NoParameterTest() // Assert - Assert.True(command.CanExecute(null)); - Assert.True(command2.CanExecute(true)); + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command2.CanExecute(true)); } - [Fact] + [Test] public void AsyncValueCommandParameter_CanExecuteFalse_NoParameter_Test() { // Arrange @@ -123,7 +122,7 @@ public void AsyncValueCommandParameter_CanExecuteFalse_NoParameter_Test() Assert.False(command2.CanExecute("Hello World")); } - [Fact] + [Test] public void AsyncValueCommandNoParameter_CanExecuteTrue_Test() { // Arrange @@ -132,10 +131,10 @@ public void AsyncValueCommandNoParameter_CanExecuteTrue_Test() // Act // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); } - [Fact] + [Test] public void AsyncValueCommandNoParameter_CanExecuteFalse_Test() { // Arrange @@ -147,7 +146,7 @@ public void AsyncValueCommandNoParameter_CanExecuteFalse_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] public void AsyncValueCommandNoParameter_CanExecuteTrueNoParameter_Test() { // Arrange @@ -156,10 +155,10 @@ public void AsyncValueCommandNoParameter_CanExecuteTrueNoParameter_Test() // Act // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); } - [Fact] + [Test] public void AsyncValueCommandNoParameter_CanExecuteFalseNoParameter_Test() { // Arrange @@ -171,24 +170,28 @@ public void AsyncValueCommandNoParameter_CanExecuteFalseNoParameter_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] public void AsyncValueCommandNoParameter_NoCanExecute_Test() { // Arrange - Func canExecute = null; + Func? canExecute = null; +#pragma warning disable CS8604 // Possible null reference argument. var command = new AsyncValueCommand(NoParameterTask, canExecute); +#pragma warning restore CS8604 // Possible null reference argument. // Act // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task AsyncValueCommand_RaiseCanExecuteChanged_Test() { // Arrange - var handleCanExecuteChangedTCS = new TaskCompletionSource(); + var semaphoreSlim = new SemaphoreSlim(1, 1); + var handleCanExecuteChangedTCS = new TaskCompletionSource(); var canCommandExecute = false; var didCanExecuteChangeFire = false; @@ -196,7 +199,7 @@ public async Task AsyncValueCommand_RaiseCanExecuteChanged_Test() var command = new AsyncValueCommand(NoParameterTask, commandCanExecute); command.CanExecuteChanged += handleCanExecuteChanged; - bool commandCanExecute(object parameter) => canCommandExecute; + bool commandCanExecute(object? parameter) => canCommandExecute; Assert.False(command.CanExecute(null)); @@ -204,29 +207,41 @@ public async Task AsyncValueCommand_RaiseCanExecuteChanged_Test() canCommandExecute = true; // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); Assert.False(didCanExecuteChangeFire); // Act command.RaiseCanExecuteChanged(); - await handleCanExecuteChangedTCS.Task; + await handleCanExecuteChangedTCS.Task.ConfigureAwait(false); // Assert - Assert.True(didCanExecuteChangeFire); - Assert.True(command.CanExecute(null)); + Assert.IsTrue(didCanExecuteChangeFire); + Assert.IsTrue(command.CanExecute(null)); - void handleCanExecuteChanged(object sender, EventArgs e) + async void handleCanExecuteChanged(object? sender, EventArgs e) { - didCanExecuteChangeFire = true; - handleCanExecuteChangedTCS.SetResult(null); + await semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try + { + command.CanExecuteChanged -= handleCanExecuteChanged; + didCanExecuteChangeFire = true; + handleCanExecuteChangedTCS.SetResult(null); + } + finally + { + semaphoreSlim.Release(); + } } } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task AsyncValueCommand_ChangeCanExecute_Test() { // Arrange - var handleCanExecuteChangedTCS = new TaskCompletionSource(); + var semaphoreSlim = new SemaphoreSlim(1, 1); + var handleCanExecuteChangedTCS = new TaskCompletionSource(); var canCommandExecute = false; var didCanExecuteChangeFire = false; @@ -234,7 +249,7 @@ public async Task AsyncValueCommand_ChangeCanExecute_Test() var command = new AsyncValueCommand(NoParameterTask, commandCanExecute); command.CanExecuteChanged += handleCanExecuteChanged; - bool commandCanExecute(object parameter) => canCommandExecute; + bool commandCanExecute(object? parameter) => canCommandExecute; Assert.False(command.CanExecute(null)); @@ -242,27 +257,36 @@ public async Task AsyncValueCommand_ChangeCanExecute_Test() canCommandExecute = true; // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); Assert.False(didCanExecuteChangeFire); // Act -#pragma warning disable CS0618 // Type or member is obsolete command.ChangeCanExecute(); - await handleCanExecuteChangedTCS.Task; -#pragma warning restore CS0618 // Type or member is obsolete + await handleCanExecuteChangedTCS.Task.ConfigureAwait(false); // Assert - Assert.True(didCanExecuteChangeFire); - Assert.True(command.CanExecute(null)); + Assert.IsTrue(didCanExecuteChangeFire); + Assert.IsTrue(command.CanExecute(null)); - void handleCanExecuteChanged(object sender, EventArgs e) + async void handleCanExecuteChanged(object? sender, EventArgs e) { - didCanExecuteChangeFire = true; - handleCanExecuteChangedTCS.SetResult(null); + await semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try + { + command.CanExecuteChanged -= handleCanExecuteChanged; + didCanExecuteChangeFire = true; + handleCanExecuteChangedTCS.SetResult(null); + } + finally + { + semaphoreSlim.Release(); + } } } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task AsyncValueCommand_Parameter_CanExecuteChanged_AllowsMultipleExecutions_Test() { // Arrange @@ -271,35 +295,56 @@ public async Task AsyncValueCommand_Parameter_CanExecuteChanged_AllowsMultipleEx var command = new AsyncValueCommand(IntParameterTask); command.CanExecuteChanged += handleCanExecuteChanged; - void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + void handleCanExecuteChanged(object? sender, EventArgs e) => canExecuteChangedCount++; - Assert.True(command.AllowsMultipleExecutions); + Assert.IsTrue(command.AllowsMultipleExecutions); // Act var asyncCommandTask = command.ExecuteAsync(Delay); // Assert - Assert.True(command.IsExecuting); - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.IsExecuting); + Assert.IsTrue(command.CanExecute(null)); // Act - await asyncCommandTask; + await asyncCommandTask.ConfigureAwait(false); // Assert - Assert.True(command.CanExecute(null)); - Assert.Equal(0, canExecuteChangedCount); + Assert.IsTrue(command.CanExecute(null)); + Assert.AreEqual(0, canExecuteChangedCount); + + command.CanExecuteChanged -= handleCanExecuteChanged; } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task AsyncValueCommand_Parameter_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test() { // Arrange + var semaphoreSlim = new SemaphoreSlim(1, 1); var canExecuteChangedCount = 0; + var handleCanExecuteChangedTCS = new TaskCompletionSource(); var command = new AsyncValueCommand(IntParameterTask, allowsMultipleExecutions: false); command.CanExecuteChanged += handleCanExecuteChanged; - void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + async void handleCanExecuteChanged(object? sender, EventArgs e) + { + await semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try + { + if (++canExecuteChangedCount is 2) + { + command.CanExecuteChanged -= handleCanExecuteChanged; + handleCanExecuteChangedTCS.SetResult(canExecuteChangedCount); + } + } + finally + { + semaphoreSlim.Release(); + } + } Assert.False(command.AllowsMultipleExecutions); @@ -307,18 +352,21 @@ public async Task AsyncValueCommand_Parameter_CanExecuteChanged_DoesNotAllowMult var asyncCommandTask = command.ExecuteAsync(Delay); // Assert - Assert.True(command.IsExecuting); + Assert.IsTrue(command.IsExecuting); Assert.False(command.CanExecute(null)); // Act - await asyncCommandTask; + await asyncCommandTask.ConfigureAwait(false); + var handleCanExecuteChangedResult = await handleCanExecuteChangedTCS.Task.ConfigureAwait(false); // Assert - Assert.True(command.CanExecute(null)); - Assert.Equal(2, canExecuteChangedCount); + Assert.IsTrue(command.CanExecute(null)); + Assert.AreEqual(2, canExecuteChangedCount); + Assert.AreEqual(canExecuteChangedCount, handleCanExecuteChangedResult); } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task AsyncValueCommand_NoParameter_CanExecuteChanged_AllowsMultipleExecutions_Test() { // Arrange @@ -327,35 +375,56 @@ public async Task AsyncValueCommand_NoParameter_CanExecuteChanged_AllowsMultiple var command = new AsyncValueCommand(() => IntParameterTask(Delay)); command.CanExecuteChanged += handleCanExecuteChanged; - void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + void handleCanExecuteChanged(object? sender, EventArgs e) => canExecuteChangedCount++; - Assert.True(command.AllowsMultipleExecutions); + Assert.IsTrue(command.AllowsMultipleExecutions); // Act var asyncCommandTask = command.ExecuteAsync(); // Assert - Assert.True(command.IsExecuting); - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.IsExecuting); + Assert.IsTrue(command.CanExecute(null)); // Act - await asyncCommandTask; + await asyncCommandTask.ConfigureAwait(false); // Assert - Assert.True(command.CanExecute(null)); - Assert.Equal(0, canExecuteChangedCount); + Assert.IsTrue(command.CanExecute(null)); + Assert.AreEqual(0, canExecuteChangedCount); + + command.CanExecuteChanged -= handleCanExecuteChanged; } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task AsyncValueCommand_NoParameter_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test() { // Arrange + var semaphoreSlim = new SemaphoreSlim(1, 1); var canExecuteChangedCount = 0; + var handleCanExecuteChangedTCS = new TaskCompletionSource(); var command = new AsyncValueCommand(() => IntParameterTask(Delay), allowsMultipleExecutions: false); command.CanExecuteChanged += handleCanExecuteChanged; - void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + async void handleCanExecuteChanged(object? sender, EventArgs e) + { + await semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try + { + if (++canExecuteChangedCount is 2) + { + command.CanExecuteChanged -= handleCanExecuteChanged; + handleCanExecuteChangedTCS.SetResult(canExecuteChangedCount); + } + } + finally + { + semaphoreSlim.Release(); + } + } Assert.False(command.AllowsMultipleExecutions); @@ -363,15 +432,17 @@ public async Task AsyncValueCommand_NoParameter_CanExecuteChanged_DoesNotAllowMu var asyncCommandTask = command.ExecuteAsync(); // Assert - Assert.True(command.IsExecuting); + Assert.IsTrue(command.IsExecuting); Assert.False(command.CanExecute(null)); // Act - await asyncCommandTask; + await asyncCommandTask.ConfigureAwait(false); + var handleCanExecuteChangedResult = await handleCanExecuteChangedTCS.Task.ConfigureAwait(false); // Assert - Assert.True(command.CanExecute(null)); - Assert.Equal(2, canExecuteChangedCount); + Assert.IsTrue(command.CanExecute(null)); + Assert.AreEqual(2, canExecuteChangedCount); + Assert.AreEqual(canExecuteChangedCount, handleCanExecuteChangedResult); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/BaseAsyncValueCommandTests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/BaseAsyncValueCommandTests.cs index a92271fca..359dbd0d7 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/BaseAsyncValueCommandTests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/BaseAsyncValueCommandTests.cs @@ -9,14 +9,12 @@ public abstract class BaseAsyncValueCommandTests : BaseCommandTests protected new ValueTask IntParameterTask(int delay) => ValueTaskDelay(delay); - protected new ValueTask StringParameterTask(string text) => ValueTaskDelay(Delay); + protected new ValueTask StringParameterTask(string? text) => ValueTaskDelay(Delay); protected new ValueTask NoParameterImmediateNullReferenceExceptionTask() => throw new NullReferenceException(); protected new ValueTask ParameterImmediateNullReferenceExceptionTask(int delay) => throw new NullReferenceException(); - protected async ValueTask ValueTaskDelay(int delay) => await Task.Delay(delay); - protected new async ValueTask NoParameterDelayedNullReferenceExceptionTask() { await Task.Delay(Delay); @@ -28,5 +26,7 @@ protected new async ValueTask IntParameterDelayedNullReferenceExceptionTask(int await Task.Delay(delay); throw new NullReferenceException(); } + + ValueTask ValueTaskDelay(int delay) => new ValueTask(Task.Delay(delay)); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/IAsyncValueCommand_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/IAsyncValueCommand_Tests.cs index 59266ab80..da70c0f94 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/IAsyncValueCommand_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/IAsyncValueCommand_Tests.cs @@ -1,13 +1,13 @@ using System.Threading.Tasks; using Xamarin.CommunityToolkit.Exceptions; using Xamarin.CommunityToolkit.ObjectModel; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.AsyncValueCommandTests { public class IAsyncValueCommandTests : BaseAsyncValueCommandTests { - [Fact] + [Test] public void IAsyncCommand_CanExecute_InvalidReferenceParameter() { // Arrange @@ -19,7 +19,7 @@ public void IAsyncCommand_CanExecute_InvalidReferenceParameter() Assert.Throws(() => command.CanExecute("Hello World")); } - [Fact] + [Test] public void IAsyncCommand_Execute_InvalidValueTypeParameter() { // Arrange @@ -31,7 +31,7 @@ public void IAsyncCommand_Execute_InvalidValueTypeParameter() Assert.Throws(() => command.Execute(true)); } - [Fact] + [Test] public void IAsyncCommand_Execute_InvalidReferenceParameter() { // Arrange @@ -43,7 +43,7 @@ public void IAsyncCommand_Execute_InvalidReferenceParameter() Assert.Throws(() => command.Execute("Hello World")); } - [Fact] + [Test] public void IAsyncCommand_CanExecute_InvalidValueTypeParameter() { // Arrange @@ -55,9 +55,8 @@ public void IAsyncCommand_CanExecute_InvalidValueTypeParameter() Assert.Throws(() => command.CanExecute(true)); } - [Theory] - [InlineData(500)] - [InlineData(0)] + [TestCase(500)] + [TestCase(0)] public async Task AsyncValueCommand_ExecuteAsync_IntParameter_Test(int parameter) { // Arrange @@ -65,15 +64,14 @@ public async Task AsyncValueCommand_ExecuteAsync_IntParameter_Test(int parameter IAsyncValueCommand command2 = new AsyncValueCommand(IntParameterTask); // Act - await command.ExecuteAsync(parameter); - await command2.ExecuteAsync(parameter); + await command.ExecuteAsync(parameter).ConfigureAwait(false); + await command2.ExecuteAsync(parameter).ConfigureAwait(false); // Assert } - [Theory] - [InlineData("Hello")] - [InlineData(default)] + [TestCase("Hello")] + [TestCase(default)] public async Task AsyncValueCommand_ExecuteAsync_StringParameter_Test(string parameter) { // Arrange @@ -81,13 +79,13 @@ public async Task AsyncValueCommand_ExecuteAsync_StringParameter_Test(string par IAsyncValueCommand command2 = new AsyncValueCommand(StringParameterTask); // Act - await command.ExecuteAsync(parameter); - await command2.ExecuteAsync(parameter); + await command.ExecuteAsync(parameter).ConfigureAwait(false); + await command2.ExecuteAsync(parameter).ConfigureAwait(false); // Assert } - [Fact] + [Test] public void IAsyncValueCommand_Parameter_CanExecuteTrue_Test() { // Arrange @@ -97,11 +95,11 @@ public void IAsyncValueCommand_Parameter_CanExecuteTrue_Test() // Act // Assert - Assert.True(command.CanExecute(null)); - Assert.True(command.CanExecute("Hello World")); + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute("Hello World")); } - [Fact] + [Test] public void IAsyncValueCommand_Parameter_CanExecuteFalse_Test() { // Arrange @@ -115,7 +113,7 @@ public void IAsyncValueCommand_Parameter_CanExecuteFalse_Test() Assert.False(command2.CanExecute(true)); } - [Fact] + [Test] public void IAsyncValueCommand_Parameter_CanExecuteTrue_NoParameter_Test() { // Arrange @@ -125,11 +123,11 @@ public void IAsyncValueCommand_Parameter_CanExecuteTrue_NoParameter_Test() // Act // Assert - Assert.True(command.CanExecute(null)); - Assert.True(command.CanExecute("Hello World")); + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute("Hello World")); } - [Fact] + [Test] public void IAsyncValueCommand_Parameter_CanExecuteFalse_NoParameter_Test() { // Arrange @@ -143,7 +141,7 @@ public void IAsyncValueCommand_Parameter_CanExecuteFalse_NoParameter_Test() Assert.False(command2.CanExecute(true)); } - [Fact] + [Test] public void IAsyncValueCommand_NoParameter_CanExecuteTrue_Test() { // Arrange @@ -152,10 +150,10 @@ public void IAsyncValueCommand_NoParameter_CanExecuteTrue_Test() // Act // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); } - [Fact] + [Test] public void IAsyncValueCommand_NoParameter_CanExecuteFalse_Test() { // Arrange @@ -167,7 +165,7 @@ public void IAsyncValueCommand_NoParameter_CanExecuteFalse_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] public void IAsyncValueCommand_NoParameter_CanExecuteTrue_NoParameter_Test() { // Arrange @@ -176,10 +174,10 @@ public void IAsyncValueCommand_NoParameter_CanExecuteTrue_NoParameter_Test() // Act // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); } - [Fact] + [Test] public void IAsyncValueCommand_NoParameter_CanExecuteFalse_NoParameter_Test() { // Arrange diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/ICommand_AsyncValueCommand_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/ICommand_AsyncValueCommand_Tests.cs index fc7265ffa..43edd576d 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/ICommand_AsyncValueCommand_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/AsyncValueCommandTests/ICommand_AsyncValueCommand_Tests.cs @@ -1,17 +1,17 @@ using System; +using System.Threading; using System.Threading.Tasks; using System.Windows.Input; using Xamarin.CommunityToolkit.Exceptions; using Xamarin.CommunityToolkit.ObjectModel; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.AsyncValueCommandTests { public class ICommand_AsyncValueCommandTests : BaseAsyncValueCommandTests { - [Theory] - [InlineData(500)] - [InlineData(0)] + [TestCase(500)] + [TestCase(0)] public async Task ICommand_Execute_IntParameter_Test(int parameter) { // Arrange @@ -19,14 +19,13 @@ public async Task ICommand_Execute_IntParameter_Test(int parameter) // Act command.Execute(parameter); - await NoParameterTask(); + await NoParameterTask().ConfigureAwait(false); // Assert } - [Theory] - [InlineData("Hello")] - [InlineData(default)] + [TestCase("Hello")] + [TestCase(default)] public async Task ICommand_Execute_StringParameter_Test(string parameter) { // Arrange @@ -34,16 +33,16 @@ public async Task ICommand_Execute_StringParameter_Test(string parameter) // Act command.Execute(parameter); - await NoParameterTask(); + await NoParameterTask().ConfigureAwait(false); // Assert } - [Fact] + [Test] public async Task ICommand_Execute_InvalidValueTypeParameter_Test() { // Arrange - InvalidCommandParameterException actualInvalidCommandParameterException = null; + InvalidCommandParameterException? actualInvalidCommandParameterException = null; var expectedInvalidCommandParameterException = new InvalidCommandParameterException(typeof(string), typeof(int)); ICommand command = new AsyncValueCommand(StringParameterTask); @@ -52,8 +51,8 @@ public async Task ICommand_Execute_InvalidValueTypeParameter_Test() try { command.Execute(Delay); - await NoParameterTask(); - await NoParameterTask(); + await NoParameterTask().ConfigureAwait(false); + await NoParameterTask().ConfigureAwait(false); } catch (InvalidCommandParameterException e) { @@ -61,15 +60,15 @@ public async Task ICommand_Execute_InvalidValueTypeParameter_Test() } // Assert - Assert.NotNull(actualInvalidCommandParameterException); - Assert.Equal(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); + Assert.IsNotNull(actualInvalidCommandParameterException); + Assert.AreEqual(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); } - [Fact] + [Test] public async Task ICommand_Execute_InvalidReferenceTypeParameter_Test() { // Arrange - InvalidCommandParameterException actualInvalidCommandParameterException = null; + InvalidCommandParameterException? actualInvalidCommandParameterException = null; var expectedInvalidCommandParameterException = new InvalidCommandParameterException(typeof(int), typeof(string)); ICommand command = new AsyncValueCommand(IntParameterTask); @@ -78,8 +77,8 @@ public async Task ICommand_Execute_InvalidReferenceTypeParameter_Test() try { command.Execute("Hello World"); - await NoParameterTask(); - await NoParameterTask(); + await NoParameterTask().ConfigureAwait(false); + await NoParameterTask().ConfigureAwait(false); } catch (InvalidCommandParameterException e) { @@ -87,15 +86,15 @@ public async Task ICommand_Execute_InvalidReferenceTypeParameter_Test() } // Assert - Assert.NotNull(actualInvalidCommandParameterException); - Assert.Equal(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); + Assert.IsNotNull(actualInvalidCommandParameterException); + Assert.AreEqual(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); } - [Fact] + [Test] public async Task ICommand_Execute_ValueTypeParameter_Test() { // Arrange - InvalidCommandParameterException actualInvalidCommandParameterException = null; + InvalidCommandParameterException? actualInvalidCommandParameterException = null; var expectedInvalidCommandParameterException = new InvalidCommandParameterException(typeof(int)); ICommand command = new AsyncValueCommand(IntParameterTask); @@ -104,8 +103,8 @@ public async Task ICommand_Execute_ValueTypeParameter_Test() try { command.Execute(null); - await NoParameterTask(); - await NoParameterTask(); + await NoParameterTask().ConfigureAwait(false); + await NoParameterTask().ConfigureAwait(false); } catch (InvalidCommandParameterException e) { @@ -113,11 +112,11 @@ public async Task ICommand_Execute_ValueTypeParameter_Test() } // Assert - Assert.NotNull(actualInvalidCommandParameterException); - Assert.Equal(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); + Assert.IsNotNull(actualInvalidCommandParameterException); + Assert.AreEqual(expectedInvalidCommandParameterException.Message, actualInvalidCommandParameterException?.Message); } - [Fact] + [Test] public void ICommand_Parameter_CanExecuteTrue_Test() { // Arrange @@ -126,10 +125,10 @@ public void ICommand_Parameter_CanExecuteTrue_Test() // Act // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); } - [Fact] + [Test] public void ICommand_Parameter_CanExecuteFalse_Test() { // Arrange @@ -141,7 +140,7 @@ public void ICommand_Parameter_CanExecuteFalse_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] public void ICommand_NoParameter_CanExecuteFalse_Test() { // Arrange @@ -153,7 +152,7 @@ public void ICommand_NoParameter_CanExecuteFalse_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] public void ICommand_Parameter_CanExecuteTrue_NoParameter_Test() { // Arrange @@ -162,10 +161,10 @@ public void ICommand_Parameter_CanExecuteTrue_NoParameter_Test() // Act // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); } - [Fact] + [Test] public void ICommand_Parameter_CanExecuteFalse_NoParameter_Test() { // Arrange @@ -177,7 +176,7 @@ public void ICommand_Parameter_CanExecuteFalse_NoParameter_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] public void ICommand_NoParameter_CanExecuteFalse_NoParameter_Test() { // Arrange @@ -189,7 +188,7 @@ public void ICommand_NoParameter_CanExecuteFalse_NoParameter_Test() Assert.False(command.CanExecute(null)); } - [Fact] + [Test] public void ICommand_Parameter_CanExecuteDynamic_Test() { // Arrange @@ -198,11 +197,11 @@ public void ICommand_Parameter_CanExecuteDynamic_Test() // Act // Assert - Assert.True(command.CanExecute(true)); + Assert.IsTrue(command.CanExecute(true)); Assert.False(command.CanExecute(false)); } - [Fact] + [Test] public void ICommand_Parameter_CanExecuteChanged_Test() { // Arrange @@ -211,11 +210,12 @@ public void ICommand_Parameter_CanExecuteChanged_Test() // Act // Assert - Assert.True(command.CanExecute(true)); + Assert.IsTrue(command.CanExecute(true)); Assert.False(command.CanExecute(false)); } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task ICommand_Parameter_CanExecuteChanged_AllowsMultipleExecutions_Test() { // Arrange @@ -224,32 +224,53 @@ public async Task ICommand_Parameter_CanExecuteChanged_AllowsMultipleExecutions_ ICommand command = new AsyncValueCommand(IntParameterTask); command.CanExecuteChanged += handleCanExecuteChanged; - void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + void handleCanExecuteChanged(object? sender, EventArgs e) => canExecuteChangedCount++; // Act command.Execute(Delay); // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); // Act - await IntParameterTask(Delay); + await IntParameterTask(Delay).ConfigureAwait(false); // Assert - Assert.True(command.CanExecute(null)); - Assert.Equal(0, canExecuteChangedCount); + Assert.IsTrue(command.CanExecute(null)); + Assert.AreEqual(0, canExecuteChangedCount); + + command.CanExecuteChanged -= handleCanExecuteChanged; } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task ICommand_Parameter_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test() { // Arrange + var semaphoreSlim = new SemaphoreSlim(1, 1); var canExecuteChangedCount = 0; + var handleCanExecuteChangedTCS = new TaskCompletionSource(); ICommand command = new AsyncValueCommand(IntParameterTask, allowsMultipleExecutions: false); command.CanExecuteChanged += handleCanExecuteChanged; - void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + async void handleCanExecuteChanged(object? sender, EventArgs e) + { + await semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try + { + if (++canExecuteChangedCount is 2) + { + command.CanExecuteChanged -= handleCanExecuteChanged; + handleCanExecuteChangedTCS.SetResult(canExecuteChangedCount); + } + } + finally + { + semaphoreSlim.Release(); + } + } // Act command.Execute(Delay); @@ -258,15 +279,16 @@ public async Task ICommand_Parameter_CanExecuteChanged_DoesNotAllowMultipleExecu Assert.False(command.CanExecute(null)); // Act - await IntParameterTask(Delay); - await IntParameterTask(Delay); + var handleCanExecuteChangedResult = await handleCanExecuteChangedTCS.Task.ConfigureAwait(false); // Assert - Assert.True(command.CanExecute(null)); - Assert.Equal(2, canExecuteChangedCount); + Assert.IsTrue(command.CanExecute(null)); + Assert.AreEqual(2, canExecuteChangedCount); + Assert.AreEqual(canExecuteChangedCount, handleCanExecuteChangedResult); } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task ICommand_NoParameter_CanExecuteChanged_AllowsMultipleExecutions_Test() { // Arrange @@ -275,33 +297,54 @@ public async Task ICommand_NoParameter_CanExecuteChanged_AllowsMultipleExecution ICommand command = new AsyncValueCommand(() => IntParameterTask(Delay)); command.CanExecuteChanged += handleCanExecuteChanged; - void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + void handleCanExecuteChanged(object? sender, EventArgs e) => canExecuteChangedCount++; // Act command.Execute(null); // Assert - Assert.True(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(null)); // Act - await IntParameterTask(Delay); - await IntParameterTask(Delay); + await IntParameterTask(Delay).ConfigureAwait(false); + await IntParameterTask(Delay).ConfigureAwait(false); // Assert - Assert.True(command.CanExecute(null)); - Assert.Equal(0, canExecuteChangedCount); + Assert.IsTrue(command.CanExecute(null)); + Assert.AreEqual(0, canExecuteChangedCount); + + command.CanExecuteChanged -= handleCanExecuteChanged; } - [Fact] + [Test] + [Timeout(ICommandTestTimeout)] public async Task ICommand_NoParameter_CanExecuteChanged_DoesNotAllowMultipleExecutions_Test() { // Arrange + var semaphoreSlim = new SemaphoreSlim(1, 1); var canExecuteChangedCount = 0; + var handleCanExecuteChangedTCS = new TaskCompletionSource(); ICommand command = new AsyncValueCommand(() => IntParameterTask(Delay), allowsMultipleExecutions: false); command.CanExecuteChanged += handleCanExecuteChanged; - void handleCanExecuteChanged(object sender, EventArgs e) => canExecuteChangedCount++; + async void handleCanExecuteChanged(object? sender, EventArgs e) + { + await semaphoreSlim.WaitAsync().ConfigureAwait(false); + + try + { + if (++canExecuteChangedCount is 2) + { + command.CanExecuteChanged -= handleCanExecuteChanged; + handleCanExecuteChangedTCS.SetResult(canExecuteChangedCount); + } + } + finally + { + semaphoreSlim.Release(); + } + } // Act command.Execute(null); @@ -310,12 +353,12 @@ public async Task ICommand_NoParameter_CanExecuteChanged_DoesNotAllowMultipleExe Assert.False(command.CanExecute(null)); // Act - await IntParameterTask(Delay); - await IntParameterTask(Delay); + var handleCanExecuteChangedResult = await handleCanExecuteChangedTCS.Task.ConfigureAwait(false); // Assert - Assert.True(command.CanExecute(null)); - Assert.Equal(2, canExecuteChangedCount); + Assert.IsTrue(command.CanExecute(null)); + Assert.AreEqual(2, canExecuteChangedCount); + Assert.AreEqual(canExecuteChangedCount, handleCanExecuteChangedResult); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/BaseCommandTests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/BaseCommandTests.cs index 7731a43ab..2954b876c 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/BaseCommandTests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/BaseCommandTests.cs @@ -2,11 +2,14 @@ using System.Threading.Tasks; using Xamarin.CommunityToolkit.UnitTests.Mocks; using Xamarin.Forms; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests { + [NonParallelizable] public abstract class BaseCommandTests { + public const int ICommandTestTimeout = Delay * 6; public const int Delay = 500; public BaseCommandTests() => Device.PlatformServices = new MockPlatformServices(); @@ -15,12 +18,24 @@ public abstract class BaseCommandTests protected Task IntParameterTask(int delay) => Task.Delay(delay); - protected Task StringParameterTask(string text) => Task.Delay(Delay); + protected Task StringParameterTask(string? text) => Task.Delay(Delay); protected Task NoParameterImmediateNullReferenceExceptionTask() => throw new NullReferenceException(); protected Task ParameterImmediateNullReferenceExceptionTask(int delay) => throw new NullReferenceException(); + protected void NoParameterAction() + { + } + + protected void ObjectParameterAction(object parameter) + { + } + + protected void IntParameterAction(int parameter) + { + } + protected async Task NoParameterDelayedNullReferenceExceptionTask() { await Task.Delay(Delay); @@ -37,19 +52,21 @@ protected async Task IntParameterDelayedNullReferenceExceptionTask(int delay) protected bool CanExecuteFalse() => false; + protected bool CanExecuteTrue(int parameter) => true; + protected bool CanExecuteTrue(bool parameter) => true; - protected bool CanExecuteTrue(string parameter) => true; + protected bool CanExecuteTrue(string? parameter) => true; - protected bool CanExecuteTrue(object parameter) => true; + protected bool CanExecuteTrue(object? parameter) => true; protected bool CanExecuteFalse(bool parameter) => false; - protected bool CanExecuteFalse(string parameter) => false; + protected bool CanExecuteFalse(string? parameter) => false; - protected bool CanExecuteFalse(object parameter) => false; + protected bool CanExecuteFalse(object? parameter) => false; - protected bool CanExecuteDynamic(object booleanParameter) + protected bool CanExecuteDynamic(object? booleanParameter) { if (booleanParameter is bool parameter) return parameter; diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/CommandFactoryTests/CommandFactory_AsyncCommand_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/CommandFactoryTests/CommandFactory_AsyncCommand_Tests.cs new file mode 100644 index 000000000..c4dbd6316 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/CommandFactoryTests/CommandFactory_AsyncCommand_Tests.cs @@ -0,0 +1,280 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Input; +using Xamarin.CommunityToolkit.Exceptions; +using Xamarin.CommunityToolkit.ObjectModel; +using NUnit.Framework; + +namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.CommandFactoryTests +{ + public class CommandFactoryAsyncCommandTests : BaseCommandTests + { + [Test] + public void AsyncCommand_NullExecuteParameter() + { + // Arrange + Func? execute = null; + + // Act + + // Assert +#pragma warning disable CS8604 // Possible null reference argument. + Assert.Throws(() => CommandFactory.Create(execute)); +#pragma warning restore CS8604 // Possible null reference argument. + } + + [Test] + public async Task AsyncCommand_NullCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(NoParameterTask); + + // Act + await command.ExecuteAsync(); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf(command); + } + + [Test] + public async Task AsyncCommand_ObjectCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(NoParameterTask, parameter => true); + + // Act + await command.ExecuteAsync(); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf(command); + } + + [Test] + public void AsyncCommand_FuncBool_NullExecuteParameter() + { + // Arrange + Func? execute = null; + + // Act + + // Assert +#pragma warning disable CS8604 // Possible null reference argument. + Assert.Throws(() => CommandFactory.Create(execute, () => true)); +#pragma warning restore CS8604 // Possible null reference argument. + } + + [Test] + public async Task AsyncCommand_FuncBool_NullCanExecuteParameter() + { + // Arrange + Func? canExecute = null; +#pragma warning disable CS8604 // Possible null reference argument. + var command = CommandFactory.Create(NoParameterTask, canExecute); +#pragma warning restore CS8604 // Possible null reference argument. + + // Act + await command.ExecuteAsync(); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf(command); + Assert.IsInstanceOf(command); + } + + [Test] + public async Task AsyncCommand_FuncBool_ValidExecuteParameter_ValidCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(NoParameterTask, () => true); + + // Act + await command.ExecuteAsync(); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf(command); + Assert.IsInstanceOf(command); + } + + [Test] + public void AsyncCommandT_NullExecuteParameter() + { + // Arrange + Func? execute = null; + + // Act + + // Assert +#pragma warning disable CS8604 // Possible null reference argument. + Assert.Throws(() => CommandFactory.Create(execute)); +#pragma warning restore CS8604 // Possible null reference argument. + } + + [Test] + public async Task AsyncCommandT_NullCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(IntParameterTask); + + // Act + await command.ExecuteAsync(0); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf>(command); + Assert.IsInstanceOf>(command); + } + + [Test] + public async Task AsyncCommandT_ObjectCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(IntParameterTask, parameter => true); + + // Act + await command.ExecuteAsync(0); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf>(command); + Assert.IsInstanceOf>(command); + } + + [Test] + public void AsyncCommandT_FuncBool_NullExecuteParameter() + { + // Arrange + Func? execute = null; + + // Act + + // Assert +#pragma warning disable CS8604 // Possible null reference argument. + Assert.Throws(() => CommandFactory.Create(execute, () => true)); +#pragma warning restore CS8604 // Possible null reference argument. + } + + [Test] + public async Task AsyncCommandT_FuncBool_NullCanExecuteParameter() + { + // Arrange + Func? canExecute = null; +#pragma warning disable CS8604 // Possible null reference argument. + var command = CommandFactory.Create(IntParameterTask, canExecute); +#pragma warning restore CS8604 // Possible null reference argument. + + // Act + await command.ExecuteAsync(0); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf>(command); + Assert.IsInstanceOf>(command); + } + + [Test] + public async Task AsyncCommandT_FuncBool_ValidExecuteParameter_ValidCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(IntParameterTask, () => true); + + // Act + await command.ExecuteAsync(0); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf>(command); + Assert.IsInstanceOf>(command); + } + + [Test] + public void AsyncCommandTExecuteTCanExecute_NullExecuteParameter() + { + // Arrange + Func? execute = null; + + // Act + + // Assert +#pragma warning disable CS8604 // Possible null reference argument. + Assert.Throws(() => CommandFactory.Create(execute)); +#pragma warning restore CS8604 // Possible null reference argument. + } + + [Test] + public async Task AsyncCommandTExecuteTCanExecute_NullCanExecuteParameter() + { + // Arrange +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + var command = CommandFactory.Create(IntParameterTask, null); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + + // Act + await command.ExecuteAsync(0); + + // Assert + Assert.IsTrue(command.CanExecute(true)); + + Assert.Throws(() => command.CanExecute(0)); + Assert.Throws(() => command.CanExecute(string.Empty)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf>(command); + Assert.IsInstanceOf>(command); + } + + [Test] + public async Task AsyncCommandTExecuteTCanExecute_ObjectCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(IntParameterTask, CanExecuteTrue); + + // Act + await command.ExecuteAsync(0); + + // Assert + Assert.IsTrue(command.CanExecute(true)); + + Assert.Throws(() => command.CanExecute(0)); + Assert.Throws(() => command.CanExecute(string.Empty)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf>(command); + Assert.IsInstanceOf>(command); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/CommandFactoryTests/CommandFactory_AsyncValueCommand_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/CommandFactoryTests/CommandFactory_AsyncValueCommand_Tests.cs new file mode 100644 index 000000000..0261a922b --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/CommandFactoryTests/CommandFactory_AsyncValueCommand_Tests.cs @@ -0,0 +1,279 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Input; +using Xamarin.CommunityToolkit.Exceptions; +using Xamarin.CommunityToolkit.ObjectModel; +using Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.AsyncValueCommandTests; +using NUnit.Framework; + +namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.CommandFactoryTests +{ + public class CommandFactoryAsyncValueCommandTests : BaseAsyncValueCommandTests + { + [Test] + public void AsyncValueCommand_NullExecuteParameter() + { + // Arrange + Func? execute = null; + + // Act + + // Assert +#pragma warning disable CS8604 // Possible null reference argument. + Assert.Throws(() => CommandFactory.Create(execute)); +#pragma warning restore CS8604 // Possible null reference argument. + } + + [Test] + public async Task AsyncValueCommand_NullCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(NoParameterTask); + + // Act + await command.ExecuteAsync(); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf(command); + } + + [Test] + public async Task AsyncValueCommand_ObjectCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(NoParameterTask, parameter => true); + + // Act + await command.ExecuteAsync(); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf(command); + } + + [Test] + public void AsyncValueCommand_FuncBool_NullExecuteParameter() + { + // Arrange + Func? execute = null; + + // Act + + // Assert +#pragma warning disable CS8604 // Possible null reference argument. + Assert.Throws(() => CommandFactory.Create(execute, () => true)); +#pragma warning restore CS8604 // Possible null reference argument. + } + + [Test] + public async Task AsyncValueCommand_FuncBool_NullCanExecuteParameter() + { + // Arrange + Func? canExecute = null; +#pragma warning disable CS8604 // Possible null reference argument. + var command = CommandFactory.Create(NoParameterTask, canExecute); +#pragma warning restore CS8604 // Possible null reference argument. + + // Act + await command.ExecuteAsync(); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf(command); + Assert.IsInstanceOf(command); + } + + [Test] + public async Task AsyncValueCommand_FuncBool_ValidExecuteParameter_ValidCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(NoParameterTask, () => true); + + // Act + await command.ExecuteAsync(); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf(command); + Assert.IsInstanceOf(command); + } + + [Test] + public void AsyncValueCommandT_NullExecuteParameter() + { + // Arrange + Func? execute = null; + + // Act + + // Assert +#pragma warning disable CS8604 // Possible null reference argument. + Assert.Throws(() => CommandFactory.Create(execute)); +#pragma warning restore CS8604 // Possible null reference argument. + } + + [Test] + public async Task AsyncValueCommandT_NullCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(IntParameterTask); + + // Act + await command.ExecuteAsync(0); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf>(command); + Assert.IsInstanceOf>(command); + } + + [Test] + public async Task AsyncValueCommandT_ObjectCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(IntParameterTask, parameter => true); + + // Act + await command.ExecuteAsync(0); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf>(command); + Assert.IsInstanceOf>(command); + } + + [Test] + public void AsyncValueCommandT_FuncBool_NullExecuteParameter() + { + // Arrange + Func? execute = null; + + // Act + + // Assert +#pragma warning disable CS8604 // Possible null reference argument. + Assert.Throws(() => CommandFactory.Create(execute, () => true)); +#pragma warning restore CS8604 // Possible null reference argument. + } + + [Test] + public async Task AsyncValueCommandT_FuncBool_NullCanExecuteParameter() + { + // Arrange + Func? canExecute = null; +#pragma warning disable CS8604 // Possible null reference argument. + var command = CommandFactory.Create(IntParameterTask, canExecute); +#pragma warning restore CS8604 // Possible null reference argument. + + // Act + await command.ExecuteAsync(0); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf>(command); + Assert.IsInstanceOf>(command); + } + + [Test] + public async Task AsyncValueCommandT_FuncBool_ValidExecuteParameter_ValidCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(IntParameterTask, () => true); + + // Act + await command.ExecuteAsync(0); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf>(command); + Assert.IsInstanceOf>(command); + } + + [Test] + public void AsyncValueCommandTExecuteTCanExecute_NullExecuteParameter() + { + // Arrange + Func? execute = null; + + // Act + + // Assert +#pragma warning disable CS8604 // Possible null reference argument. + Assert.Throws(() => CommandFactory.Create(execute)); +#pragma warning restore CS8604 // Possible null reference argument. + } + + [Test] + public async Task AsyncValueCommandTExecuteTCanExecute_NullCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(IntParameterTask, null); + + // Act + await command.ExecuteAsync(0); + + // Assert + Assert.IsTrue(command.CanExecute(true)); + + Assert.Throws(() => command.CanExecute(0)); + Assert.Throws(() => command.CanExecute(string.Empty)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf>(command); + Assert.IsInstanceOf>(command); + } + + [Test] + public async Task AsyncValueCommandTExecuteTCanExecute_ObjectCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(IntParameterTask, CanExecuteTrue); + + // Act + await command.ExecuteAsync(0); + + // Assert + Assert.IsTrue(command.CanExecute(true)); + + Assert.Throws(() => command.CanExecute(0)); + Assert.Throws(() => command.CanExecute(string.Empty)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf>(command); + Assert.IsInstanceOf>(command); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/CommandFactoryTests/CommandFactory_Command_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/CommandFactoryTests/CommandFactory_Command_Tests.cs new file mode 100644 index 000000000..96dbbcf22 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ICommandTests/CommandFactoryTests/CommandFactory_Command_Tests.cs @@ -0,0 +1,182 @@ +using System; +using System.Windows.Input; +using Xamarin.CommunityToolkit.ObjectModel; +using NUnit.Framework; + +namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel.ICommandTests.CommandFactoryTests +{ + public class CommandFactoryCommandTests : BaseCommandTests + { + [Test] + public void Action_NullExecuteParameter() + { + // Arrange + Action? execute = null; + + // Act + + // Assert +#pragma warning disable CS8604 // Possible null reference argument. + Assert.Throws(() => CommandFactory.Create(execute)); +#pragma warning restore CS8604 // Possible null reference argument. + } + + [Test] + public void Action_ValidExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(NoParameterAction); + + // Act + command.Execute(null); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf(command); + } + + [Test] + public void Action_NullCanExecuteParameter() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + Assert.Throws(() => CommandFactory.Create(NoParameterAction, null)); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + } + + [Test] + public void Action_ValidCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(NoParameterAction, CanExecuteTrue); + + // Act + command.Execute(null); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf(command); + } + + [Test] + public void ActionObject_NullExecuteParameter() + { + // Arrange + Action? execute = null; + + // Act + + // Assert +#pragma warning disable CS8604 // Possible null reference argument. + Assert.Throws(() => CommandFactory.Create(execute)); +#pragma warning restore CS8604 // Possible null reference argument. + } + + [Test] + public void ActionObject_ValidExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(ObjectParameterAction); + + // Act + command.Execute(null); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf(command); + } + + [Test] + public void ActionObject_NullCanExecuteParameter() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + Assert.Throws(() => CommandFactory.Create(ObjectParameterAction, null)); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + } + + [Test] + public void ActionObject_ValidCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(ObjectParameterAction, _ => true); + + // Act + command.Execute(1); + command.Execute(null); + command.Execute(string.Empty); + + // Assert + Assert.IsTrue(command.CanExecute(null)); + Assert.IsTrue(command.CanExecute(string.Empty)); + Assert.IsTrue(command.CanExecute(0)); + + Assert.IsInstanceOf(command); + Assert.IsInstanceOf(command); + } + + [Test] + public void ActionInt_NullExecuteParameter() + { + // Arrange + Action? execute = null; + + // Act + + // Assert +#pragma warning disable CS8604 // Possible null reference argument. + Assert.Throws(() => CommandFactory.Create(execute, CanExecuteTrue)); +#pragma warning restore CS8604 // Possible null reference argument. + } + + [Test] + public void ActionInt_NullCanExecuteParameter() + { + // Arrange + + // Act + + // Assert +#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + Assert.Throws(() => CommandFactory.Create(IntParameterAction, null)); +#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + } + + [Test] + public void ActionInt_ValidCanExecuteParameter() + { + // Arrange + var command = CommandFactory.Create(IntParameterAction, CanExecuteTrue); + + // Act + + // Assert + Assert.IsTrue(command.CanExecute(0)); + Assert.False(command.CanExecute(null)); + Assert.False(command.CanExecute(string.Empty)); + + Assert.IsInstanceOf>(command); + Assert.IsInstanceOf(command); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ObservableObject_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ObservableObject_Tests.cs index f86ab782f..f3f23f530 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ObservableObject_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ObservableObject_Tests.cs @@ -1,28 +1,22 @@ using System; using System.ComponentModel; using System.Threading.Tasks; -using Xamarin.CommunityToolkit.ObjectModel; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel { public sealed class ObservableObject_Tests { - Person person; - - public ObservableObject_Tests() + [Test] + public void OnPropertyChanged() { - person = new Person + var person = new Person { FirstName = "James", LastName = "Montemagno" }; - } - [Fact] - public void OnPropertyChanged() - { - PropertyChangedEventArgs updated = null; + PropertyChangedEventArgs? updated = null; person.PropertyChanged += (sender, args) => { updated = args; @@ -30,14 +24,20 @@ public void OnPropertyChanged() person.FirstName = "Motz"; - Assert.NotNull(updated); - Assert.Equal(nameof(person.FirstName), updated.PropertyName); + Assert.IsNotNull(updated); + Assert.AreEqual(nameof(person.FirstName), updated?.PropertyName); } - [Fact] + [Test] public void OnDidntChange() { - PropertyChangedEventArgs updated = null; + var person = new Person + { + FirstName = "James", + LastName = "Montemagno" + }; + + PropertyChangedEventArgs? updated = null; person.PropertyChanged += (sender, args) => { updated = args; @@ -48,9 +48,15 @@ public void OnDidntChange() Assert.Null(updated); } - [Fact] + [Test] public void OnChangedEvent() { + var person = new Person + { + FirstName = "James", + LastName = "Montemagno" + }; + var triggered = false; person.Changed = () => { @@ -59,12 +65,18 @@ public void OnChangedEvent() person.FirstName = "Motz"; - Assert.True(triggered, "OnChanged didn't raise"); + Assert.IsTrue(triggered, "OnChanged didn't raise"); } - [Fact] + [Test] public void OnChangingEvent() { + var person = new Person + { + FirstName = "James", + LastName = "Montemagno" + }; + var triggered = false; person.Changing = () => { @@ -73,12 +85,18 @@ public void OnChangingEvent() person.FirstName = "Motz"; - Assert.True(triggered, "OnChanging didn't raise"); + Assert.IsTrue(triggered, "OnChanging didn't raise"); } - [Fact] + [Test] public void ValidateEvent() { + var person = new Person + { + FirstName = "James", + LastName = "Montemagno" + }; + var contol = "Motz"; var triggered = false; person.Validate = (oldValue, newValue) => @@ -89,13 +107,19 @@ public void ValidateEvent() person.FirstName = contol; - Assert.True(triggered, "ValidateValue didn't raise"); - Assert.Equal(person.FirstName, contol); + Assert.IsTrue(triggered, "ValidateValue didn't raise"); + Assert.AreEqual(person.FirstName, contol); } - [Fact] + [Test] public void NotValidateEvent() { + var person = new Person + { + FirstName = "James", + LastName = "Montemagno" + }; + var contol = person.FirstName; var triggered = false; person.Validate = (oldValue, newValue) => @@ -106,49 +130,31 @@ public void NotValidateEvent() person.FirstName = "Motz"; - Assert.True(triggered, "ValidateValue didn't raise"); - Assert.Equal(person.FirstName, contol); + Assert.IsTrue(triggered, "ValidateValue didn't raise"); + Assert.AreEqual(person.FirstName, contol); } - [Fact] - public async Task ValidateEventException() + [Test] + public void ValidateEventException() { + var person = new Person + { + FirstName = "James", + LastName = "Montemagno" + }; + person.Validate = (oldValue, newValue) => { throw new ArgumentOutOfRangeException(); }; - var result = await Assert.ThrowsAsync(() => + var result = Assert.ThrowsAsync(() => { person.FirstName = "Motz"; return Task.CompletedTask; }); - Assert.NotNull(result); - } - - public class Person : ObservableObject - { - string firstName; - string lastName; - - public Action Changed { get; set; } - - public Action Changing { get; set; } - - public Func Validate { get; set; } - - public string FirstName - { - get => firstName; - set => SetProperty(ref firstName, value, onChanged: Changed, onChanging: Changing, validateValue: Validate); - } - - public string LastName - { - get => lastName; - set => SetProperty(ref lastName, value, onChanged: Changed, onChanging: Changing, validateValue: Validate); - } + Assert.IsNotNull(result); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ObservableRangeCollection_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ObservableRangeCollection_Tests.cs index 9b98b339b..3c5b3bbb1 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ObservableRangeCollection_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/ObservableRangeCollection_Tests.cs @@ -1,14 +1,15 @@ +using System; using System.Collections.Generic; using System.Collections.Specialized; using Xamarin.CommunityToolkit.ObjectModel; -using Xunit; -using Xunit.Sdk; +using NUnit.Framework; +using NUnit.Framework.Internal; namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel { public class ObservableRangeCollection_Tests { - [Fact] + [Test] public void AddRange() { var collection = new ObservableRangeCollection(); @@ -16,19 +17,14 @@ public void AddRange() collection.CollectionChanged += (s, e) => { - Assert.Equal(NotifyCollectionChangedAction.Add, e.Action); + Assert.AreEqual(NotifyCollectionChangedAction.Add, e.Action); Assert.Null(e.OldItems); - Assert.Equal(toAdd.Length, e.NewItems.Count); - - for (var i = 0; i < toAdd.Length; i++) - { - Assert.Equal(toAdd[i], (int)e.NewItems[i]); - } + Assert.AreEqual(toAdd, e.NewItems); }; collection.AddRange(toAdd); } - [Fact] + [Test] public void AddRangeEmpty() { var collection = new ObservableRangeCollection(); @@ -36,12 +32,12 @@ public void AddRangeEmpty() collection.CollectionChanged += (s, e) => { - throw new XunitException("The event is raised."); + throw new NUnitException("The event is raised."); }; collection.AddRange(toAdd); } - [Fact] + [Test] public void ReplaceRange() { var collection = new ObservableRangeCollection(); @@ -50,23 +46,23 @@ public void ReplaceRange() collection.AddRange(toRemove); collection.CollectionChanged += (s, e) => { - Assert.Equal(NotifyCollectionChangedAction.Reset, e.Action); + Assert.AreEqual(NotifyCollectionChangedAction.Reset, e.Action); Assert.Null(e.OldItems); Assert.Null(e.NewItems); - Assert.Equal(collection.Count, toAdd.Length); + Assert.AreEqual(collection.Count, toAdd.Length); for (var i = 0; i < toAdd.Length; i++) { if (collection[i] != (int)toAdd[i]) - throw new XunitException("Expected and actual items don't match."); + throw new NUnitException("Expected and actual items don't match."); } }; collection.ReplaceRange(toAdd); } - [Fact] + [Test] public void ReplaceRange_on_non_empty_collection_should_always_raise_collection_changes() { var collection = new ObservableRangeCollection(new[] { 1 }); @@ -79,10 +75,10 @@ public void ReplaceRange_on_non_empty_collection_should_always_raise_collection_ }; collection.ReplaceRange(toAdd); - Assert.True(eventRaised, "Collection Reset should be raised."); + Assert.IsTrue(eventRaised, "Collection Reset should be raised."); } - [Fact] + [Test] public void ReplaceRange_on_empty_collection_should_NOT_raise_collection_changes_when_empty() { var collection = new ObservableRangeCollection(); @@ -90,13 +86,13 @@ public void ReplaceRange_on_empty_collection_should_NOT_raise_collection_changes collection.CollectionChanged += (s, e) => { - throw new XunitException("Collection changes should NOT be raised."); + throw new NUnitException("Collection changes should NOT be raised."); }; collection.ReplaceRange(toAdd); } - [Fact] + [Test] public void ReplaceRange_should_NOT_mutate_source() { var sourceData = new List(new[] { 1, 2, 3 }); @@ -104,10 +100,10 @@ public void ReplaceRange_should_NOT_mutate_source() collection.ReplaceRange(sourceData); - Assert.Equal(3, sourceData.Count); + Assert.AreEqual(3, sourceData.Count); } - [Fact] + [Test] public void RemoveRangeRemoveFact() { var collection = new ObservableRangeCollection(); @@ -117,23 +113,25 @@ public void RemoveRangeRemoveFact() collection.CollectionChanged += (s, e) => { if (e.Action != NotifyCollectionChangedAction.Remove) - throw new XunitException("RemoveRange didn't use Remove like requested."); + throw new NUnitException("RemoveRange didn't use Remove like requested."); + if (e.OldItems == null) - throw new XunitException("OldItems should not be null."); + throw new NUnitException("OldItems should not be null."); + var expected = new int[] { 1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 7, 8, 9, 9 }; if (expected.Length != e.OldItems.Count) - throw new XunitException("Expected and actual OldItems don't match."); + throw new NUnitException("Expected and actual OldItems don't match."); + for (var i = 0; i < expected.Length; i++) { - if (expected[i] != (int)e.OldItems[i]) - throw new XunitException("Expected and actual OldItems don't match."); + if (expected[i] != (int?)e.OldItems[i]) + throw new NUnitException("Expected and actual OldItems don't match."); } }; collection.RemoveRange(toRemove, NotifyCollectionChangedAction.Remove); - } - [Fact] + [Test] public void RemoveRangeEmpty() { var collection = new ObservableRangeCollection(); @@ -142,12 +140,12 @@ public void RemoveRangeEmpty() collection.AddRange(toAdd); collection.CollectionChanged += (s, e) => { - throw new XunitException("The event is raised."); + throw new NUnitException("The event is raised."); }; collection.RemoveRange(toRemove, NotifyCollectionChangedAction.Remove); } - [Fact] + [Test] public void RemoveRange_should_NOT_mutate_source_when_source_data_is_not_present() { var sourceData = new List(new[] { 1, 2, 3 }); @@ -155,10 +153,10 @@ public void RemoveRange_should_NOT_mutate_source_when_source_data_is_not_present collection.RemoveRange(sourceData, NotifyCollectionChangedAction.Remove); - Assert.Equal(3, sourceData.Count); + Assert.AreEqual(3, sourceData.Count); } - [Fact] + [Test] public void RemoveRange_should_NOT_mutate_source_when_source_data_is_present() { var sourceData = new List(new[] { 1, 2, 3 }); @@ -166,10 +164,10 @@ public void RemoveRange_should_NOT_mutate_source_when_source_data_is_present() collection.RemoveRange(sourceData, NotifyCollectionChangedAction.Remove); - Assert.Equal(3, sourceData.Count); + Assert.AreEqual(3, sourceData.Count); } - [Fact] + [Test] public void RemoveRange_should_NOT_mutate_collection_when_source_data_is_not_present() { var sourceData = new List(new[] { 1, 2, 3 }); @@ -178,7 +176,43 @@ public void RemoveRange_should_NOT_mutate_collection_when_source_data_is_not_pre collection.RemoveRange(sourceData, NotifyCollectionChangedAction.Remove); // the collection should not be modified if the source items are not found - Assert.Equal(6, collection.Count); + Assert.AreEqual(6, collection.Count); + } + + [Test] + public void AddCollection() + { + var toAdd = new[] { 3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3, 2, 3 }; + + var wrapper = new CollectionWrapper() + { + Collection = { toAdd } + }; + + Assert.AreEqual(toAdd, wrapper.Collection); + } + + [Test] + public void AddToNullCollection() + { + var toAdd = new[] { 3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9, 3, 2, 3 }; + +#pragma warning disable CS8670 // Object or collection initializer implicitly dereferences possibly null member. + Assert.Throws(() => + { + var wrapper = new CollectionWrapper() + { + NullCollection = { toAdd } + }; + }); +#pragma warning restore CS8670 // Object or collection initializer implicitly dereferences possibly null member. + } + + class CollectionWrapper + { + public ObservableRangeCollection Collection { get; } = new ObservableRangeCollection(); + + public ObservableRangeCollection? NullCollection { get; init; } } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/Person.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/Person.cs new file mode 100644 index 000000000..25993c9cc --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/ObjectModel/Person.cs @@ -0,0 +1,31 @@ +using System; +using Xamarin.CommunityToolkit.ObjectModel; + +namespace Xamarin.CommunityToolkit.UnitTests.ObjectModel +{ + class Person : ObservableObject + { + string firstName = string.Empty; + string lastName = string.Empty; + + public Action? Changed { get; set; } + + public Action? Changing { get; set; } + + public Func? Validate { get; set; } + + public string FirstName + { + get => firstName; + set => SetProperty(ref firstName, value, onChanged: Changed, onChanging: Changing, validateValue: Validate); + } + + public string LastName + { + get => lastName; + set => SetProperty(ref lastName, value, onChanged: Changed, onChanging: Changing, validateValue: Validate); + } + + public string Group => FirstName[0].ToString().ToUpperInvariant(); + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Views/CameraView_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Views/CameraView_Tests.cs index e0cae8a47..1a89d6475 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Views/CameraView_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Views/CameraView_Tests.cs @@ -1,29 +1,29 @@ using System; using Xamarin.CommunityToolkit.UI.Views; using Xamarin.Forms; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Views { public class CameraView_Tests { - [Fact] + [Test] public void TestConstructor() { var camera = new CameraView(); Assert.False(camera.IsBusy); Assert.False(camera.IsAvailable); - Assert.Equal(CameraOptions.Default, camera.CameraOptions); + Assert.AreEqual(CameraOptions.Default, camera.CameraOptions); // See TODO on CameraView.SavePhotoToFile // Assert.False(camera.SavePhotoToFile); - Assert.Equal(CameraCaptureMode.Default, camera.CaptureMode); - Assert.Equal(CameraFlashMode.Off, camera.FlashMode); + Assert.AreEqual(CameraCaptureMode.Default, camera.CaptureMode); + Assert.AreEqual(CameraFlashMode.Off, camera.FlashMode); } - [Fact] + [Test] public void TestOnMediaCaptured() { var camera = new CameraView(); @@ -33,10 +33,10 @@ public void TestOnMediaCaptured() camera.MediaCaptured += (_, e) => fired = e == args; camera.RaiseMediaCaptured(args); - Assert.True(fired); + Assert.IsTrue(fired); } - [Fact] + [Test] public void TestOnMediaCapturedFailed() { var camera = new CameraView(); @@ -45,10 +45,10 @@ public void TestOnMediaCapturedFailed() camera.MediaCaptureFailed += (_, e) => fired = e == "123"; camera.RaiseMediaCaptureFailed("123"); - Assert.True(fired); + Assert.IsTrue(fired); } - [Fact] + [Test] public void TestOnShutterClicked() { var camera = new CameraView(); @@ -57,10 +57,10 @@ public void TestOnShutterClicked() camera.ShutterClicked += (sender, e) => fired = true; camera.Shutter(); - Assert.True(fired); + Assert.IsTrue(fired); } - [Fact] + [Test] public void TestShutterCommand() { var camera = new CameraView(); @@ -71,12 +71,12 @@ public void TestShutterCommand() trigged = true; }; - camera.ShutterCommand.Execute(null); + camera.ShutterCommand?.Execute(null); - Assert.True(trigged); + Assert.IsTrue(trigged); } - [Fact] + [Test] public void TestShutterCommandFromVM() { var vm = new CameraViewModel(); @@ -96,7 +96,7 @@ public void TestShutterCommandFromVM() vm.ShutterCommand.Execute(null); - Assert.True(trigged); + Assert.IsTrue(trigged); } class CameraViewModel @@ -112,7 +112,7 @@ public CameraViewModel() CameraShutterCommand = new Command(DoNothing); } - void Shutter() => CameraShutterCommand?.Execute(null); + void Shutter() => CameraShutterCommand.Execute(null); void DoNothing() => Console.WriteLine("This is just to prove that the user can't override the CameraViewShutterCommand"); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Views/MediaElement_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Views/MediaElement_Tests.cs index 6b94c4e45..4ce235a95 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Views/MediaElement_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Views/MediaElement_Tests.cs @@ -1,13 +1,13 @@ using System; using Xamarin.CommunityToolkit.Core; using Xamarin.CommunityToolkit.UI.Views; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Views { public class MediaElement_Tests { - [Fact] + [Test] public void TestSource() { var mediaElement = new MediaElement(); @@ -24,11 +24,11 @@ public void TestSource() var source = MediaSource.FromFile("Video.mp4"); mediaElement.Source = source; - Assert.Equal(source, mediaElement.Source); - Assert.True(signaled); + Assert.AreEqual(source, mediaElement.Source); + Assert.IsTrue(signaled); } - [Fact] + [Test] public void TestSourceDoubleSet() { var mediaElement = new MediaElement { Source = MediaSource.FromFile("Video.mp4") }; @@ -45,7 +45,7 @@ public void TestSourceDoubleSet() Assert.False(signaled); } - [Fact] + [Test] public void TestFileMediaSourceChanged() { var source = (FileMediaSource)MediaSource.FromFile("Video.mp4"); @@ -57,32 +57,35 @@ public void TestFileMediaSourceChanged() }; source.File = "Other.mp4"; - Assert.Equal("Other.mp4", source.File); + Assert.AreEqual("Other.mp4", source.File); - Assert.True(signaled); + Assert.IsTrue(signaled); } - [Fact] + [Test] public void TestSourceRoundTrip() { var uri = new Uri("https://sec.ch9.ms/ch9/5d93/a1eab4bf-3288-4faf-81c4-294402a85d93/XamarinShow_mid.mp4"); var media = new MediaElement(); + Assert.Null(media.Source); + media.Source = uri; - Assert.NotNull(media.Source); - Assert.IsType(media.Source); - Assert.Equal(uri, ((UriMediaSource)media.Source).Uri); + + Assert.IsNotNull(media.Source); + Assert.IsInstanceOf(media.Source); + Assert.AreEqual(uri, ((UriMediaSource?)media.Source)?.Uri); } - [Fact] + [Test] public void TestDefaultValueForShowsPlaybackControls() { var media = new MediaElement(); - Assert.True(media.ShowsPlaybackControls); + Assert.IsTrue(media.ShowsPlaybackControls); } - [Fact] + [Test] public void TestShowsPlaybackControlsSet() { var media = new MediaElement { ShowsPlaybackControls = false }; diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Views/MediaSource_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Views/MediaSource_Tests.cs index 013c75f9b..c91bf6d80 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Views/MediaSource_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Views/MediaSource_Tests.cs @@ -1,124 +1,139 @@ using System; using Xamarin.Forms; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Views { public class MediaSource_Tests { - [Fact] + [Test] public void TestConstructors() { var filesource = new Core.FileMediaSource { File = "File.mp4" }; - Assert.Equal("File.mp4", filesource.File); + Assert.AreEqual("File.mp4", filesource.File); var urisource = new Core.UriMediaSource { Uri = new Uri("http://xamarin.com/media.mp4") }; - Assert.Equal("http://xamarin.com/media.mp4", urisource.Uri.AbsoluteUri); + Assert.AreEqual("http://xamarin.com/media.mp4", urisource.Uri.AbsoluteUri); } - [Fact] + [Test] public void TestHelpers() { var mediasource = Core.MediaSource.FromFile("File.mp4"); - Assert.IsType(mediasource); - Assert.Equal("File.mp4", ((Core.FileMediaSource)mediasource).File); + Assert.IsInstanceOf(mediasource); + Assert.AreEqual("File.mp4", ((Core.FileMediaSource)mediasource).File); var urisource = Core.MediaSource.FromUri(new Uri("http://xamarin.com/media.mp4")); - Assert.IsType(urisource); - Assert.Equal("http://xamarin.com/media.mp4", ((Core.UriMediaSource)urisource).Uri.AbsoluteUri); + Assert.IsInstanceOf(urisource); + Assert.AreEqual("http://xamarin.com/media.mp4", ((Core.UriMediaSource?)urisource)?.Uri?.AbsoluteUri); } - [Fact] + [Test] public void TestImplicitFileConversion() { var mediaElement = new UI.Views.MediaElement { Source = "File.mp4" }; - Assert.True(mediaElement.Source != null); - Assert.IsType(mediaElement.Source); - Assert.Equal("File.mp4", ((Core.FileMediaSource)mediaElement.Source).File); + + Assert.IsNotNull(mediaElement.Source); + Assert.IsInstanceOf(mediaElement.Source); + Assert.AreEqual("File.mp4", ((Core.FileMediaSource?)mediaElement.Source)?.File); } - [Fact] + [Test] public void TestImplicitStringConversionWhenNull() { - string s = null; - var sut = (Core.MediaSource)s; - Assert.IsType(sut); - Assert.Null(((Core.FileMediaSource)sut).File); + string? s = null; + var sut = (Core.MediaSource?)s; + + Assert.IsInstanceOf(sut); + Assert.Null(((Core.FileMediaSource?)sut)?.File); } - [Fact] + [Test] public void TestImplicitUriConversion() { var mediaElement = new UI.Views.MediaElement { Source = new Uri("http://xamarin.com/media.mp4") }; - Assert.True(mediaElement.Source != null); - Assert.IsType(mediaElement.Source); - Assert.Equal("http://xamarin.com/media.mp4", ((Core.UriMediaSource)mediaElement.Source).Uri.AbsoluteUri); + + Assert.IsNotNull(mediaElement.Source); + Assert.IsInstanceOf(mediaElement.Source); + Assert.AreEqual("http://xamarin.com/media.mp4", ((Core.UriMediaSource?)mediaElement.Source)?.Uri?.AbsoluteUri); } - [Fact] + [Test] public void TestImplicitStringUriConversion() { var mediaElement = new UI.Views.MediaElement { Source = "http://xamarin.com/media.mp4" }; - Assert.True(mediaElement.Source != null); - Assert.IsType(mediaElement.Source); - Assert.Equal("http://xamarin.com/media.mp4", ((Core.UriMediaSource)mediaElement.Source).Uri.AbsoluteUri); + + Assert.IsNotNull(mediaElement.Source); + Assert.IsInstanceOf(mediaElement.Source); + Assert.AreEqual("http://xamarin.com/media.mp4", ((Core.UriMediaSource?)mediaElement.Source)?.Uri?.AbsoluteUri); } - [Fact] + [Test] public void TestImplicitUriConversionWhenNull() { - Uri u = null; - var sut = (Core.MediaSource)u; + Uri? u = null; + var sut = (Core.MediaSource?)u; + Assert.Null(sut); } - [Fact] + [Test] public void TestSetStringValue() { var mediaElement = new UI.Views.MediaElement(); mediaElement.SetValue(UI.Views.MediaElement.SourceProperty, "media.mp4"); - Assert.NotNull(mediaElement.Source); - Assert.IsType(mediaElement.Source); - Assert.Equal("media.mp4", ((Core.FileMediaSource)mediaElement.Source).File); + + Assert.IsNotNull(mediaElement.Source); + Assert.IsInstanceOf(mediaElement.Source); + Assert.AreEqual("media.mp4", ((Core.FileMediaSource?)mediaElement.Source)?.File); } - [Fact] + [Test] public void TextBindToStringValue() { var mediaElement = new UI.Views.MediaElement(); mediaElement.SetBinding(UI.Views.MediaElement.SourceProperty, "."); + Assert.Null(mediaElement.Source); + mediaElement.BindingContext = "media.mp4"; - Assert.NotNull(mediaElement.Source); - Assert.IsType(mediaElement.Source); - Assert.Equal("media.mp4", ((Core.FileMediaSource)mediaElement.Source).File); + + Assert.IsNotNull(mediaElement.Source); + Assert.IsInstanceOf(mediaElement.Source); + Assert.AreEqual("media.mp4", ((Core.FileMediaSource?)mediaElement.Source)?.File); } - [Fact] + [Test] public void TextBindToStringUriValue() { var mediaElement = new UI.Views.MediaElement(); mediaElement.SetBinding(UI.Views.MediaElement.SourceProperty, "."); + Assert.Null(mediaElement.Source); + mediaElement.BindingContext = "http://xamarin.com/media.mp4"; - Assert.NotNull(mediaElement.Source); - Assert.IsType(mediaElement.Source); - Assert.Equal("http://xamarin.com/media.mp4", ((Core.UriMediaSource)mediaElement.Source).Uri.AbsoluteUri); + + Assert.IsNotNull(mediaElement.Source); + Assert.IsInstanceOf(mediaElement.Source); + Assert.AreEqual("http://xamarin.com/media.mp4", ((Core.UriMediaSource?)mediaElement.Source)?.Uri?.AbsoluteUri); } - [Fact] + [Test] public void TextBindToUriValue() { var mediaElement = new UI.Views.MediaElement(); mediaElement.SetBinding(UI.Views.MediaElement.SourceProperty, "."); + Assert.Null(mediaElement.Source); + mediaElement.BindingContext = new Uri("http://xamarin.com/media.mp4"); - Assert.NotNull(mediaElement.Source); - Assert.IsType(mediaElement.Source); - Assert.Equal("http://xamarin.com/media.mp4", ((Core.UriMediaSource)mediaElement.Source).Uri.AbsoluteUri); + + Assert.IsNotNull(mediaElement.Source); + Assert.IsInstanceOf(mediaElement.Source); + Assert.AreEqual("http://xamarin.com/media.mp4", ((Core.UriMediaSource?)mediaElement.Source)?.Uri?.AbsoluteUri); } - [Fact] + [Test] public void TestBindingContextPropagation() { var context = new object(); @@ -128,29 +143,33 @@ public void TestBindingContextPropagation() }; var source = new MockMediaSource(); mediaElement.Source = source; - Assert.Same(context, source.BindingContext); + + Assert.AreSame(context, source.BindingContext); mediaElement = new UI.Views.MediaElement(); source = new MockMediaSource(); mediaElement.Source = source; mediaElement.BindingContext = context; - Assert.Same(context, source.BindingContext); + + Assert.AreSame(context, source.BindingContext); } - [Fact] + [Test] public void ImplicitCastOnAbsolutePathsShouldCreateAFileMediaSource() { var path = "/private/var/mobile/Containers/Data/Application/B1E5AB19-F815-4B4A-AB97-BD4571D53743/Documents/temp/video.mp4"; var mediaElement = new UI.Views.MediaElement { Source = path }; - Assert.IsType(mediaElement.Source); + + Assert.IsInstanceOf(mediaElement.Source); } - [Fact] + [Test] public void ImplicitCastOnWindowsAbsolutePathsShouldCreateAFileMediaSource() { var path = "C:\\Users\\Username\\Videos\\video.mp4"; var mediaElement = new UI.Views.MediaElement { Source = path }; - Assert.IsType(mediaElement.Source); + + Assert.IsInstanceOf(mediaElement.Source); } class MockMediaSource : Core.MediaSource diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Views/SnackBar_Tests.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Views/SnackBar_Tests.cs index b9901f0e5..f32be4615 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Views/SnackBar_Tests.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Views/SnackBar_Tests.cs @@ -1,42 +1,41 @@ using System; using System.Threading.Tasks; -using NSubstitute; using Xamarin.CommunityToolkit.Extensions; using Xamarin.CommunityToolkit.UI.Views.Options; using Xamarin.Forms; -using Xunit; +using NUnit.Framework; namespace Xamarin.CommunityToolkit.UnitTests.Views { public class SnackBar_Tests { #if !NETCOREAPP - [Fact] - public async void PageExtension_DisplaySnackBarAsync_PlatformNotSupportedException() + [Test] + public void PageExtension_DisplaySnackBarAsync_PlatformNotSupportedException() { var page = new ContentPage(); - await Assert.ThrowsAsync(() => page.DisplaySnackBarAsync(Arg.Any(), Arg.Any(), Arg.Any>())); + Assert.ThrowsAsync(() => page.DisplaySnackBarAsync(string.Empty, string.Empty, () => Task.CompletedTask)); } - [Fact] - public async void PageExtension_DisplaySnackBarAsyncWithOptions_PlatformNotSupportedException() + [Test] + public void PageExtension_DisplaySnackBarAsyncWithOptions_PlatformNotSupportedException() { var page = new ContentPage(); - await Assert.ThrowsAsync(() => page.DisplaySnackBarAsync(Arg.Any())); + Assert.ThrowsAsync(() => page.DisplaySnackBarAsync(new SnackBarOptions())); } - [Fact] - public async void PageExtension_DisplayToastAsync_PlatformNotSupportedException() + [Test] + public void PageExtension_DisplayToastAsync_PlatformNotSupportedException() { var page = new ContentPage(); - await Assert.ThrowsAsync(() => page.DisplayToastAsync("message")); + Assert.ThrowsAsync(() => page.DisplayToastAsync("message")); } - [Fact] - public async void PageExtension_DisplayToastAsyncWithOptions_PlatformNotSupportedException() + [Test] + public void PageExtension_DisplayToastAsyncWithOptions_PlatformNotSupportedException() { var page = new ContentPage(); - await Assert.ThrowsAsync(() => page.DisplayToastAsync(Arg.Any())); + Assert.ThrowsAsync(() => page.DisplayToastAsync(new ToastOptions())); } #endif } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Xamarin.CommunityToolkit.UnitTests.csproj b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Xamarin.CommunityToolkit.UnitTests.csproj index 506115a4f..2614e2f23 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Xamarin.CommunityToolkit.UnitTests.csproj +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit.UnitTests/Xamarin.CommunityToolkit.UnitTests.csproj @@ -2,18 +2,16 @@ netcoreapp2.1;netcoreapp3.1;net461 - false - - - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationBehavior.shared.cs index f8e147b4e..db3b5471d 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationBehavior.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationBehavior.shared.cs @@ -7,14 +7,14 @@ public class AnimationBehavior : EventToCommandBehavior public static readonly BindableProperty AnimationTypeProperty = BindableProperty.Create(nameof(AnimationType), typeof(AnimationBase), typeof(AnimationBehavior)); - public AnimationBase AnimationType + public AnimationBase? AnimationType { - get => (AnimationBase)GetValue(AnimationTypeProperty); + get => (AnimationBase?)GetValue(AnimationTypeProperty); set => SetValue(AnimationTypeProperty, value); } bool isAnimating; - TapGestureRecognizer tapGestureRecognizer; + TapGestureRecognizer? tapGestureRecognizer; protected override void OnAttachedTo(VisualElement bindable) { @@ -37,14 +37,15 @@ protected override void OnDetachingFrom(VisualElement bindable) base.OnDetachingFrom(bindable); } - protected override async void OnTriggerHandled(object sender = null, object eventArgs = null) + protected override async void OnTriggerHandled(object? sender = null, object? eventArgs = null) { if (isAnimating) return; isAnimating = true; - await AnimationType?.Animate((View)sender); + if (AnimationType != null) + await AnimationType.Animate((View?)sender); if (Command?.CanExecute(CommandParameter) ?? false) Command.Execute(CommandParameter); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/AnimationBase.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/AnimationBase.shared.cs index bfb14cdf9..9d31b42ac 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/AnimationBase.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/AnimationBase.shared.cs @@ -31,7 +31,7 @@ static object GetDefaultDurationProperty(BindableObject bindable) protected abstract uint DefaultDuration { get; set; } - public abstract Task Animate(TView view); + public abstract Task Animate(TView? view); } public abstract class AnimationBase : AnimationBase diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/FadeAnimation.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/FadeAnimation.shared.cs index 6abf1af9c..683e40c07 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/FadeAnimation.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/FadeAnimation.shared.cs @@ -16,10 +16,13 @@ public double Fade protected override uint DefaultDuration { get; set; } = 300; - public override async Task Animate(View view) + public override async Task Animate(View? view) { - await view.FadeTo(Fade, Duration, Easing); - await view.FadeTo(1, Duration, Easing); + if (view != null) + { + await view.FadeTo(Fade, Duration, Easing); + await view.FadeTo(1, Duration, Easing); + } } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/FlipHorizontalAnimation.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/FlipHorizontalAnimation.shared.cs index 295dc86ce..aeb62ebe7 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/FlipHorizontalAnimation.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/FlipHorizontalAnimation.shared.cs @@ -9,10 +9,13 @@ public class FlipHorizontalAnimation : RotateAnimation protected override uint DefaultDuration { get; set; } = 300; - public override async Task Animate(View view) + public override async Task Animate(View? view) { - await view.RotateYTo(Rotation, Duration, Easing); - await view.RotateYTo(0, Duration, Easing); + if (view != null) + { + await view.RotateYTo(Rotation, Duration, Easing); + await view.RotateYTo(0, Duration, Easing); + } } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/FlipVerticalAnimation.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/FlipVerticalAnimation.shared.cs index 9737157f8..2912b2ea2 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/FlipVerticalAnimation.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/FlipVerticalAnimation.shared.cs @@ -7,10 +7,13 @@ public class FlipVerticalAnimation : RotateAnimation { protected override double DefaultRotation { get; set; } = 90; - public override async Task Animate(View view) + public override async Task Animate(View? view) { - await view.RotateXTo(Rotation, Duration, Easing); - await view.RotateXTo(0, Duration, Easing); + if (view != null) + { + await view.RotateXTo(Rotation, Duration, Easing); + await view.RotateXTo(0, Duration, Easing); + } } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/RotateAnimation.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/RotateAnimation.shared.cs index 3b9e6ee7f..c0f6d77c2 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/RotateAnimation.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/RotateAnimation.shared.cs @@ -21,10 +21,13 @@ static object GetDefaulRotationProperty(BindableObject bindable) protected virtual double DefaultRotation { get; set; } = 180.0; - public override async Task Animate(View view) + public override async Task Animate(View? view) { - await view.RotateTo(Rotation, Duration, Easing); - view.Rotation = 0; + if (view != null) + { + await view.RotateTo(Rotation, Duration, Easing); + view.Rotation = 0; + } } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/ScaleAnimation.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/ScaleAnimation.shared.cs index e189609c0..b7c346b91 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/ScaleAnimation.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/ScaleAnimation.shared.cs @@ -16,10 +16,13 @@ public double Scale protected override uint DefaultDuration { get; set; } = 170; - public override async Task Animate(View view) + public override async Task Animate(View? view) { - await view.ScaleTo(Scale, Duration, Easing); - await view.ScaleTo(1, Duration, Easing); + if (view != null) + { + await view.ScaleTo(Scale, Duration, Easing); + await view.ScaleTo(1, Duration, Easing); + } } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/ShakeAnimation.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/ShakeAnimation.shared.cs index 16616c146..5c78e92d1 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/ShakeAnimation.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Animations/AnimationTypes/ShakeAnimation.shared.cs @@ -16,15 +16,18 @@ public double StartFactor protected override uint DefaultDuration { get; set; } = 50; - public override async Task Animate(View view) + public override async Task Animate(View? view) { - for (var i = StartFactor; i > 0; i = i - 5) + if (view != null) { - await view.TranslateTo(-i, 0, Duration, Easing); - await view.TranslateTo(i, 0, Duration, Easing); - } + for (var i = StartFactor; i > 0; i -= 5) + { + await view.TranslateTo(-i, 0, Duration, Easing); + await view.TranslateTo(i, 0, Duration, Easing); + } - view.TranslationX = 0; + view.TranslationX = 0; + } } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/AttachedBehaviors/SetFocusOnEntryCompletedBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/AttachedBehaviors/SetFocusOnEntryCompletedBehavior.shared.cs new file mode 100644 index 000000000..d2201789d --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/AttachedBehaviors/SetFocusOnEntryCompletedBehavior.shared.cs @@ -0,0 +1,50 @@ +using System; +using Xamarin.CommunityToolkit.Behaviors.Internals; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.Behaviors +{ + /// + /// The is an attached property for entries that allows the user to specify what should gain focus after the user completes that entry. + /// + public class SetFocusOnEntryCompletedBehavior : BaseBehavior + { + /// + /// The attached property. + /// + public static readonly BindableProperty NextElementProperty = + BindableProperty.CreateAttached( + "NextElement", + typeof(VisualElement), + typeof(SetFocusOnEntryCompletedBehavior), + default(VisualElement), + propertyChanged: OnNextElementChanged); + + /// + /// Required accessor for attached property. + /// + public static VisualElement GetNextElement(BindableObject view) => + (VisualElement)view.GetValue(NextElementProperty); + + /// + /// Required accessor for attached property. + /// + public static void SetNextElement(BindableObject view, VisualElement value) => + view.SetValue(NextElementProperty, value); + + static void OnNextElementChanged(BindableObject bindable, object oldValue, object newValue) + { + var entry = (Entry)bindable; + var weakEntry = new WeakReference(entry); + entry.Completed += completedHandler; + + void completedHandler(object? sender, EventArgs e) + { + if (weakEntry.TryGetTarget(out var origEntry)) + { + GetNextElement(origEntry)?.Focus(); + } + } + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/BaseBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/BaseBehavior.shared.cs index c331fb17a..51840d367 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/BaseBehavior.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/BaseBehavior.shared.cs @@ -11,17 +11,17 @@ namespace Xamarin.CommunityToolkit.Behaviors.Internals /// The that the behavior can be applied to public abstract class BaseBehavior : Behavior where TView : VisualElement { - static readonly MethodInfo getContextMethod + static readonly MethodInfo? getContextMethod = typeof(BindableObject).GetRuntimeMethods()?.FirstOrDefault(m => m.Name == "GetContext"); - static readonly FieldInfo bindingField + static readonly FieldInfo? bindingField = getContextMethod?.ReturnType.GetRuntimeField("Binding"); - BindingBase defaultBindingContextBinding; + BindingBase? defaultBindingContextBinding; - protected TView View { get; private set; } + protected TView? View { get; private set; } - protected virtual void OnViewPropertyChanged(object sender, PropertyChangedEventArgs e) + protected virtual void OnViewPropertyChanged(object? sender, PropertyChangedEventArgs e) { } @@ -56,7 +56,7 @@ protected override void OnDetachingFrom(TView bindable) View = null; } - protected bool IsBound(BindableProperty property, BindingBase defaultBinding = null) + protected bool IsBound(BindableProperty property, BindingBase? defaultBinding = null) { var context = getContextMethod?.Invoke(this, new object[] { property }); return context != null diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/EventToCommandBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/EventToCommandBehavior.shared.cs index 0505d0c38..d3cfed7c5 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/EventToCommandBehavior.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/EventToCommandBehavior.shared.cs @@ -36,34 +36,34 @@ public class EventToCommandBehavior : BaseBehavior public static readonly BindableProperty EventArgsConverterProperty = BindableProperty.Create(nameof(EventArgsConverter), typeof(IValueConverter), typeof(EventToCommandBehavior)); - readonly MethodInfo eventHandlerMethodInfo = typeof(EventToCommandBehavior).GetTypeInfo().GetDeclaredMethod(nameof(OnTriggerHandled)); + readonly MethodInfo eventHandlerMethodInfo = typeof(EventToCommandBehavior).GetTypeInfo()?.GetDeclaredMethod(nameof(OnTriggerHandled)) ?? throw new NullReferenceException($"Cannot find method {nameof(OnTriggerHandled)}"); - Delegate eventHandler; + Delegate? eventHandler; - EventInfo eventInfo; + EventInfo? eventInfo; /// /// The name of the event that should be associated with . This is bindable property. /// - public string EventName + public string? EventName { - get => (string)GetValue(EventNameProperty); + get => (string?)GetValue(EventNameProperty); set => SetValue(EventNameProperty, value); } /// /// The Command that should be executed when the event configured with is triggered. This is a bindable property. /// - public ICommand Command + public ICommand? Command { - get => (ICommand)GetValue(CommandProperty); + get => (ICommand?)GetValue(CommandProperty); set => SetValue(CommandProperty, value); } /// /// An optional parameter to forward to the . This is a bindable property. /// - public object CommandParameter + public object? CommandParameter { get => GetValue(CommandParameterProperty); set => SetValue(CommandParameterProperty, value); @@ -101,9 +101,10 @@ void RegisterEvent() if (View == null || string.IsNullOrWhiteSpace(eventName)) return; - eventInfo = View.GetType().GetRuntimeEvent(eventName) ?? + eventInfo = View.GetType()?.GetRuntimeEvent(eventName) ?? throw new ArgumentException($"{nameof(EventToCommandBehavior)}: Couldn't resolve the event.", nameof(EventName)); + _ = eventInfo.EventHandlerType ?? throw new NullReferenceException(); _ = eventHandlerMethodInfo ?? throw new NullReferenceException($"{nameof(eventHandlerMethodInfo)} is null, maybe it's a linker issue, please open a bug here: https://github.com/xamarin/XamarinCommunityToolkit/issues/"); eventHandler = eventHandlerMethodInfo.CreateDelegate(eventInfo.EventHandlerType, this) ?? @@ -122,7 +123,7 @@ void UnregisterEvent() } [Preserve(Conditional = true)] - protected virtual void OnTriggerHandled(object sender = null, object eventArgs = null) + protected virtual void OnTriggerHandled(object? sender = null, object? eventArgs = null) { var parameter = CommandParameter ?? EventArgsConverter?.Convert(eventArgs, typeof(object), null, null) diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/EventToCommandBehaviorGeneric.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/EventToCommandBehaviorGeneric.shared.cs new file mode 100644 index 000000000..0cba27b51 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/EventToCommandBehaviorGeneric.shared.cs @@ -0,0 +1,28 @@ +using System; + +namespace Xamarin.CommunityToolkit.Behaviors +{ + /// + /// This cast the sender object to a specific type defined by the user. + /// + /// The type that you want to receive in your + public sealed class EventToCommandBehavior : EventToCommandBehavior + { + protected override void OnTriggerHandled(object? sender = null, object? eventArgs = null) + { + var parameter = CommandParameter + ?? EventArgsConverter?.Convert(eventArgs, typeof(object), null, null) + ?? eventArgs; + + if (parameter is not TType) + { + // changing it to the default value to avoid a cast exception + parameter = default(TType); + } + + var command = Command; + if (command?.CanExecute(parameter) ?? false) + command.Execute(parameter); + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/ImpliedOrderGridBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/ImpliedOrderGridBehavior.shared.cs index ec40e4082..78a6045c5 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/ImpliedOrderGridBehavior.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/ImpliedOrderGridBehavior.shared.cs @@ -11,7 +11,7 @@ namespace Xamarin.CommunityToolkit.Behaviors /// public class ImpliedOrderGridBehavior : BaseBehavior { - bool[][] usedMatrix; + bool[][]? usedMatrix; int rowCount; int columnCount; @@ -34,7 +34,7 @@ protected override void OnDetachingFrom(Grid bindable) bindable.ChildAdded -= OnInternalGridChildAdded; } - void OnInternalGridChildAdded(object sender, ElementEventArgs e) => + void OnInternalGridChildAdded(object? sender, ElementEventArgs e) => ProcessElement(e.Element); void LogWarning(string warning) @@ -46,15 +46,20 @@ void LogWarning(string warning) bool[][] InitMatrix() { + _ = View ?? throw new NullReferenceException($"{nameof(View)} cannot be null."); + rowCount = View.RowDefinitions.Count; if (rowCount == 0) rowCount = 1; + columnCount = View.ColumnDefinitions.Count; if (columnCount == 0) columnCount = 1; + var newMatrix = new bool[rowCount][]; for (var r = 0; r < rowCount; r++) newMatrix[r] = new bool[columnCount]; + return newMatrix; } @@ -100,8 +105,9 @@ void UpdateUsedCells(int row, int column, int rowSpan, int columnSpan) { for (var c = column; c < columnEnd; c++) { - if (usedMatrix[r][c]) + if (usedMatrix?[r][c] ?? throw new NullReferenceException()) LogWarning($"Cell at row {r} column {c} has already been used."); + usedMatrix[r][c] = true; } } @@ -127,4 +133,4 @@ void ProcessElement(BindableObject view) view.SetValue(Grid.RowProperty, row); } } -} \ No newline at end of file +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/MaskedBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/MaskedBehavior.shared.cs index 9addd051c..1515b8df8 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/MaskedBehavior.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/MaskedBehavior.shared.cs @@ -23,16 +23,16 @@ public class MaskedBehavior : BaseBehavior public static readonly BindableProperty UnMaskedCharacterProperty = BindableProperty.Create(nameof(UnMaskedCharacter), typeof(char), typeof(MaskedBehavior), 'X', propertyChanged: OnUnMaskedCharacterPropertyChanged); - IDictionary positions; + IDictionary? positions; bool applyingMask; /// /// The mask that the input value needs to match. This is a bindable property. /// - public string Mask + public string? Mask { - get => (string)GetValue(MaskProperty); + get => (string?)GetValue(MaskProperty); set => SetValue(MaskProperty, value); } @@ -51,7 +51,7 @@ static void OnMaskPropertyChanged(BindableObject bindable, object oldValue, obje static void OnUnMaskedCharacterPropertyChanged(BindableObject bindable, object oldValue, object newValue) => ((MaskedBehavior)bindable).OnMaskChanged(); - protected override void OnViewPropertyChanged(object sender, PropertyChangedEventArgs e) + protected override void OnViewPropertyChanged(object? sender, PropertyChangedEventArgs e) { base.OnViewPropertyChanged(sender, e); @@ -65,7 +65,7 @@ void OnTextPropertyChanged() return; applyingMask = true; - ApplyMask(View.Text); + ApplyMask(View?.Text); applyingMask = false; } @@ -78,10 +78,13 @@ void SetPositions() } var list = new Dictionary(); - for (var i = 0; i < Mask.Length; i++) + if (Mask != null) { - if (Mask[i] != UnMaskedCharacter) - list.Add(i, Mask[i]); + for (var i = 0; i < Mask.Length; i++) + { + if (Mask[i] != UnMaskedCharacter) + list.Add(i, Mask[i]); + } } positions = list; @@ -95,16 +98,21 @@ void OnMaskChanged() return; } - var originalText = RemoveMask(View?.Text); + var originalText = RemoveMaskNullableString(View?.Text); SetPositions(); ApplyMask(originalText); } - string RemoveMask(string text) + string? RemoveMaskNullableString(string? text) { - if (string.IsNullOrEmpty(text)) + if (text == null || string.IsNullOrEmpty(text)) return text; + return RemoveMask(text); + } + + string RemoveMask(string text) + { var maskChars = positions .Select(c => c.Value) .Distinct() @@ -113,11 +121,11 @@ string RemoveMask(string text) return string.Join(string.Empty, text.Split(maskChars)); } - void ApplyMask(string text) + void ApplyMask(string? text) { - if (!string.IsNullOrWhiteSpace(text) && positions != null) + if (text != null && !string.IsNullOrWhiteSpace(text) && positions != null) { - if (text.Length > Mask.Length) + if (text.Length > (Mask?.Length ?? 0)) text = text.Remove(text.Length - 1); text = RemoveMask(text); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/MaxLengthReachedBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/MaxLengthReachedBehavior.shared.cs index 11a993787..b7d723a13 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/MaxLengthReachedBehavior.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/MaxLengthReachedBehavior.shared.cs @@ -21,9 +21,9 @@ public class MaxLengthReachedBehavior : BaseBehavior /// /// Command that is triggered when the value configured in is reached. Both the event and this command are triggered. This is a bindable property. /// - public ICommand Command + public ICommand? Command { - get => (ICommand)GetValue(CommandProperty); + get => (ICommand?)GetValue(CommandProperty); set => SetValue(CommandProperty, value); } @@ -53,7 +53,7 @@ public bool ShouldDismissKeyboardAutomatically remove => maxLengthReachedEventManager.RemoveEventHandler(value); } - protected override void OnViewPropertyChanged(object sender, PropertyChangedEventArgs e) + protected override void OnViewPropertyChanged(object? sender, PropertyChangedEventArgs e) { base.OnViewPropertyChanged(sender, e); if (e.PropertyName == InputView.TextProperty.PropertyName) @@ -62,7 +62,7 @@ protected override void OnViewPropertyChanged(object sender, PropertyChangedEven void OnTextPropertyChanged() { - if (!(View.Text?.Length >= View.MaxLength)) + if (View?.Text == null || View.Text.Length < View.MaxLength) return; if (ShouldDismissKeyboardAutomatically) diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/ProgressBarAnimationBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/ProgressBarAnimationBehavior.shared.cs new file mode 100644 index 000000000..1ec8fa31a --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/ProgressBarAnimationBehavior.shared.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using Xamarin.CommunityToolkit.Behaviors.Internals; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.Behaviors +{ + public class ProgressBarAnimationBehavior : BaseBehavior + { + /// + /// Backing BindableProperty for the property. + /// + public static readonly BindableProperty AnimateProgressProperty = + BindableProperty.CreateAttached(nameof(AnimateProgress), typeof(double), typeof(ProgressBar), 0.0d, propertyChanged: OnAnimateProgressPropertyChanged); + + public double AnimateProgress + { + get => (double)GetValue(AnimateProgressProperty); + set => SetValue(AnimateProgressProperty, value); + } + + static async void OnAnimateProgressPropertyChanged(BindableObject bindable, object oldValue, object newValue) + => await ((ProgressBarAnimationBehavior)bindable).Animate(); + + async Task Animate() + { + if (View != null) + await View.ProgressTo(AnimateProgress, 500, Easing.Linear); + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/UserStoppedTypingBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/UserStoppedTypingBehavior.shared.cs index a3aab0be5..1d163b12d 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/UserStoppedTypingBehavior.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/UserStoppedTypingBehavior.shared.cs @@ -42,21 +42,21 @@ public class UserStoppedTypingBehavior : BaseBehavior public static readonly BindableProperty ShouldDismissKeyboardAutomaticallyProperty = BindableProperty.Create(nameof(ShouldDismissKeyboardAutomatically), typeof(bool), typeof(UserStoppedTypingBehavior), false); - CancellationTokenSource tokenSource; + CancellationTokenSource? tokenSource; /// /// Command that is triggered when the is reached. When is set, it's only triggered when both conditions are met. This is a bindable property. /// - public ICommand Command + public ICommand? Command { - get => (ICommand)GetValue(CommandProperty); + get => (ICommand?)GetValue(CommandProperty); set => SetValue(CommandProperty, value); } /// /// An optional parameter to forward to the . This is a bindable property. /// - public object CommandParameter + public object? CommandParameter { get => GetValue(CommandParameterProperty); set => SetValue(CommandParameterProperty, value); @@ -89,7 +89,7 @@ public bool ShouldDismissKeyboardAutomatically set => SetValue(ShouldDismissKeyboardAutomaticallyProperty, value); } - protected override void OnViewPropertyChanged(object sender, PropertyChangedEventArgs e) + protected override void OnViewPropertyChanged(object? sender, PropertyChangedEventArgs e) { base.OnViewPropertyChanged(sender, e); if (e.PropertyName == InputView.TextProperty.PropertyName) @@ -105,17 +105,20 @@ void OnTextPropertyChanged() } tokenSource = new CancellationTokenSource(); - _ = Task.Delay(StoppedTypingTimeThreshold, tokenSource.Token) + Task.Delay(StoppedTypingTimeThreshold, tokenSource.Token) .ContinueWith(task => { + if (task.IsFaulted && task.Exception != null) + throw task.Exception; + if (task.Status == TaskStatus.Canceled || - View.Text.Length < MinimumLengthThreshold) + View?.Text?.Length < MinimumLengthThreshold) return; - if (ShouldDismissKeyboardAutomatically) + if (View != null && ShouldDismissKeyboardAutomatically) Device.BeginInvokeOnMainThread(View.Unfocus); - if (Command?.CanExecute(CommandParameter ?? View.Text) ?? false) + if (View != null && Command?.CanExecute(CommandParameter ?? View.Text) is true) Command.Execute(CommandParameter ?? View.Text); }); } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/CharactersValidationBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/CharactersValidationBehavior.shared.cs index b4a78fcb1..73b16f6cf 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/CharactersValidationBehavior.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/CharactersValidationBehavior.shared.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Xamarin.Forms; namespace Xamarin.CommunityToolkit.Behaviors @@ -10,7 +12,7 @@ namespace Xamarin.CommunityToolkit.Behaviors /// public class CharactersValidationBehavior : TextValidationBehavior { - List> characterPredicates; + List> characterPredicates = Enumerable.Empty>().ToList(); /// /// Backing BindableProperty for the property. @@ -63,8 +65,9 @@ public int MaximumCharacterCount set => SetValue(MaximumCharacterCountProperty, value); } - protected override bool Validate(object value) - => base.Validate(value) && Validate(value?.ToString()); + protected override async ValueTask ValidateAsync(object? value, CancellationToken token) + => await base.ValidateAsync(value, token).ConfigureAwait(false) + && Validate(value?.ToString()); static void OnCharacterTypePropertyChanged(BindableObject bindable, object oldValue, object newValue) { @@ -99,7 +102,7 @@ static IEnumerable> GetCharacterPredicates(CharacterType charact void OnCharacterTypePropertyChanged() => characterPredicates = GetCharacterPredicates(CharacterType).ToList(); - bool Validate(string value) + bool Validate(string? value) { var count = value?.ToCharArray().Count(character => characterPredicates.Any(predicate => predicate.Invoke(character))) ?? 0; return count >= MinimumCharacterCount diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/EmailValidationBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/EmailValidationBehavior.shared.cs index fbf89afb3..a2f090ea2 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/EmailValidationBehavior.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/EmailValidationBehavior.shared.cs @@ -21,14 +21,14 @@ protected override string DefaultRegexPattern protected override RegexOptions DefaultRegexOptions => RegexOptions.IgnoreCase; - protected override object DecorateValue() + protected override object? Decorate(object? value) { - var value = base.DecorateValue()?.ToString(); + var stringValue = base.Decorate(value)?.ToString(); #if NETSTANDARD1_0 - return value; + return stringValue; #else - if (string.IsNullOrWhiteSpace(value)) - return value; + if (string.IsNullOrWhiteSpace(stringValue)) + return stringValue; try { @@ -43,11 +43,11 @@ static string DomainMapper(Match match) } // Normalize the domain - return normalizerRegex.Replace(value, DomainMapper); + return normalizerRegex.Replace(stringValue, DomainMapper); } catch (ArgumentException) { - return value; + return stringValue; } #endif } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/MultiValidationBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/MultiValidationBehavior.shared.cs index 86bd43892..f07a7cc12 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/MultiValidationBehavior.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/MultiValidationBehavior.shared.cs @@ -2,6 +2,8 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Xamarin.CommunityToolkit.Behaviors.Internals; using Xamarin.Forms; @@ -17,13 +19,12 @@ public class MultiValidationBehavior : ValidationBehavior /// Backing BindableProperty for the property. /// public static readonly BindableProperty ErrorsProperty = - BindableProperty.Create(nameof(Errors), typeof(List), typeof(MultiValidationBehavior), null, BindingMode.OneWayToSource); + BindableProperty.Create(nameof(Errors), typeof(List), typeof(MultiValidationBehavior), null, BindingMode.OneWayToSource); public static readonly BindableProperty ErrorProperty = BindableProperty.CreateAttached(nameof(GetError), typeof(object), typeof(MultiValidationBehavior), null); - readonly ObservableCollection children - = new ObservableCollection(); + readonly ObservableCollection children = new ObservableCollection(); /// /// Constructor for this behavior. @@ -34,25 +35,23 @@ public MultiValidationBehavior() /// /// Holds the errors from all of the nested invalid validators in . This is a bindable property. /// - public List Errors + public List? Errors { - get => (List)GetValue(ErrorsProperty); + get => (List?)GetValue(ErrorsProperty); set => SetValue(ErrorsProperty, value); } /// /// All child behaviors that are part of this . This is a bindable property. /// - public IList Children - => children; + public IList Children => children; /// /// Method to extract the error from the attached property for a child behavior in . /// /// The that we extract the attached Error property /// Object containing error information - public static object GetError(BindableObject bindable) - => bindable.GetValue(ErrorProperty); + public static object? GetError(BindableObject bindable) => bindable.GetValue(ErrorProperty); /// /// Method to set the error on the attached property for a child behavior in . @@ -62,14 +61,18 @@ public static object GetError(BindableObject bindable) public static void SetError(BindableObject bindable, object value) => bindable.SetValue(ErrorProperty, value); - protected override bool Validate(object value) + protected override async ValueTask ValidateAsync(object? value, CancellationToken token) { - var errors = children.Where(c => + await Task.WhenAll(children.Select(c => { c.Value = value; - c.ForceValidate(); - return c.IsNotValid; - }).Select(c => GetError(c)); + return c.ValidateNestedAsync(token).AsTask(); + })).ConfigureAwait(false); + + if (token.IsCancellationRequested) + return IsValid; + + var errors = children.Where(c => c.IsNotValid).Select(c => GetError(c)); if (!errors.Any()) { @@ -78,12 +81,12 @@ protected override bool Validate(object value) } if (!Errors?.SequenceEqual(errors) ?? true) - Errors = errors.ToList(); + Errors = (errors ?? Enumerable.Empty()).ToList(); return false; } - void OnChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + void OnChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (e.NewItems != null) { diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/NumericValidationBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/NumericValidationBehavior.shared.cs index c182c6b29..50082b51a 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/NumericValidationBehavior.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/NumericValidationBehavior.shared.cs @@ -1,4 +1,7 @@ -using System.Globalization; +using System; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; using Xamarin.CommunityToolkit.Behaviors.Internals; using Xamarin.Forms; @@ -69,30 +72,35 @@ public int MaximumDecimalPlaces set => SetValue(MaximumDecimalPlacesProperty, value); } - protected override object DecorateValue() - => base.DecorateValue()?.ToString()?.Trim(); + protected override object? Decorate(object? value) + => base.Decorate(value)?.ToString()?.Trim(); - protected override bool Validate(object value) + protected override ValueTask ValidateAsync(object? value, CancellationToken token) { - var valueString = value as string; + if (value is not string valueString) + return new ValueTask(false); + if (!(double.TryParse(valueString, out var numeric) - && numeric >= MinimumValue - && numeric <= MaximumValue)) - return false; + && numeric >= MinimumValue + && numeric <= MaximumValue)) + { + return new ValueTask(false); + } var decimalDelimeterIndex = valueString.IndexOf(CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator); var hasDecimalDelimeter = decimalDelimeterIndex >= 0; // If MaximumDecimalPlaces equals zero, ".5" or "14." should be considered as invalid inputs. if (hasDecimalDelimeter && MaximumDecimalPlaces == 0) - return false; + return new ValueTask(false); var decimalPlaces = hasDecimalDelimeter ? valueString.Substring(decimalDelimeterIndex + 1, valueString.Length - decimalDelimeterIndex - 1).Length : 0; - return decimalPlaces >= MinimumDecimalPlaces - && decimalPlaces <= MaximumDecimalPlaces; + return new ValueTask( + decimalPlaces >= MinimumDecimalPlaces && + decimalPlaces <= MaximumDecimalPlaces); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/RequiredStringValidationBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/RequiredStringValidationBehavior.shared.cs index c27db20dc..04432a0b2 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/RequiredStringValidationBehavior.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/RequiredStringValidationBehavior.shared.cs @@ -1,4 +1,6 @@ -using Xamarin.CommunityToolkit.Behaviors.Internals; +using System.Threading; +using System.Threading.Tasks; +using Xamarin.CommunityToolkit.Behaviors.Internals; using Xamarin.Forms; namespace Xamarin.CommunityToolkit.Behaviors @@ -17,13 +19,13 @@ public class RequiredStringValidationBehavior : ValidationBehavior /// /// The string that will be compared to the value provided by the user. This is a bindable property. /// - public string RequiredString + public string? RequiredString { - get => (string)GetValue(RequiredStringProperty); + get => (string?)GetValue(RequiredStringProperty); set => SetValue(RequiredStringProperty, value); } - protected override bool Validate(object value) - => value?.ToString() == RequiredString; + protected override ValueTask ValidateAsync(object? value, CancellationToken token) + => new ValueTask(value?.ToString() == RequiredString); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/TextValidationBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/TextValidationBehavior.shared.cs index 552694210..286a6d4b1 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/TextValidationBehavior.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/TextValidationBehavior.shared.cs @@ -1,5 +1,7 @@ using System.Text; using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; using Xamarin.CommunityToolkit.Behaviors.Internals; using Xamarin.Forms; @@ -40,7 +42,7 @@ public class TextValidationBehavior : ValidationBehavior public static readonly BindableProperty RegexOptionsProperty = BindableProperty.Create(nameof(RegexOptions), typeof(RegexOptions), typeof(TextValidationBehavior), defaultValueCreator: GetDefaultRegexOptions, propertyChanged: OnRegexPropertyChanged); - Regex regex; + Regex? regex; /// /// The minimum length of the value that will be allowed. This is a bindable property. @@ -72,9 +74,9 @@ public TextDecorationFlags DecorationFlags /// /// The regular expression pattern which the value will have to match before it will be allowed. This is a bindable property. /// - public string RegexPattern + public string? RegexPattern { - get => (string)GetValue(RegexPatternProperty); + get => (string?)GetValue(RegexPatternProperty); set => SetValue(RegexPatternProperty, value); } @@ -96,39 +98,37 @@ public RegexOptions RegexOptions protected virtual RegexOptions DefaultRegexOptions => RegexOptions.None; - protected override object DecorateValue() + protected override object? Decorate(object? value) { - var value = base.DecorateValue()?.ToString(); + var stringValue = base.Decorate(value)?.ToString(); var flags = DecorationFlags; if (flags.HasFlag(TextDecorationFlags.NullToEmpty)) - value ??= string.Empty; + stringValue ??= string.Empty; - if (value == null) + if (stringValue == null) return null; if (flags.HasFlag(TextDecorationFlags.TrimStart)) - value = value.TrimStart(); + stringValue = stringValue.TrimStart(); if (flags.HasFlag(TextDecorationFlags.TrimEnd)) - value = value.TrimEnd(); + stringValue = stringValue.TrimEnd(); if (flags.HasFlag(TextDecorationFlags.NormalizeWhiteSpace)) - value = NormalizeWhiteSpace(value); + stringValue = NormalizeWhiteSpace(stringValue); - return value; + return stringValue; } - protected override bool Validate(object value) + protected override ValueTask ValidateAsync(object? value, CancellationToken token) { var text = value?.ToString(); - if (text == null) - return false; - - var length = text.Length; - return length >= MinimumLength && - length <= MaximumLength && - (regex?.IsMatch(text) ?? false); + return new ValueTask( + text != null && + text.Length >= MinimumLength && + text.Length <= MaximumLength && + (regex?.IsMatch(text) ?? false)); } static void OnRegexPropertyChanged(BindableObject bindable, object oldValue, object newValue) diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/UriValidationBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/UriValidationBehavior.shared.cs index baacfe922..6af0ff965 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/UriValidationBehavior.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/UriValidationBehavior.shared.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; using Xamarin.Forms; namespace Xamarin.CommunityToolkit.Behaviors @@ -23,8 +25,8 @@ public UriKind UriKind set => SetValue(UriKindProperty, value); } - protected override bool Validate(object value) - => base.Validate(value) + protected override async ValueTask ValidateAsync(object? value, CancellationToken token) + => await base.ValidateAsync(value, token).ConfigureAwait(false) && Uri.IsWellFormedUriString(value?.ToString(), UriKind); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/ValidationBehavior.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/ValidationBehavior.shared.cs index 4603f95ac..1994b4034 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/ValidationBehavior.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Behaviors/Validators/ValidationBehavior.shared.cs @@ -1,5 +1,8 @@ using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; using System.Windows.Input; +using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.Forms; namespace Xamarin.CommunityToolkit.Behaviors.Internals @@ -9,6 +12,12 @@ namespace Xamarin.CommunityToolkit.Behaviors.Internals /// public abstract class ValidationBehavior : BaseBehavior { + public const string ValidVisualState = "Valid"; + + public const string InvalidVisualState = "Invalid"; + + public ValidationBehavior() => DefaultForceValidateCommand = new AsyncValueCommand(ForceValidate); + /// /// Backing BindableProperty for the property. /// @@ -21,6 +30,12 @@ public abstract class ValidationBehavior : BaseBehavior public static readonly BindableProperty IsValidProperty = BindableProperty.Create(nameof(IsValid), typeof(bool), typeof(ValidationBehavior), true, BindingMode.OneWayToSource, propertyChanged: OnIsValidPropertyChanged); + /// + /// Backing BindableProperty for the property. + /// + public static readonly BindableProperty IsRunningProperty = + BindableProperty.Create(nameof(IsRunning), typeof(bool), typeof(ValidationBehavior), false, BindingMode.OneWayToSource); + /// /// Backing BindableProperty for the property. /// @@ -61,7 +76,9 @@ public abstract class ValidationBehavior : BaseBehavior bool isAttaching; - BindingBase defaultValueBinding; + BindingBase? defaultValueBinding; + + CancellationTokenSource? validationTokenSource; /// /// Indicates whether or not the current value is considered valid. This is a bindable property. @@ -72,6 +89,15 @@ public bool IsValid set => SetValue(IsValidProperty, value); } + /// + /// Indicates whether or not the validation is in progress now (waiting for an asynchronous call is finished). + /// + public bool IsRunning + { + get => (bool)GetValue(IsRunningProperty); + set => SetValue(IsRunningProperty, value); + } + /// /// Indicates whether or not the current value is considered not valid. This is a bindable property. /// @@ -111,7 +137,7 @@ public ValidationFlags Flags /// /// The value to validate. This is a bindable property. /// - public object Value + public object? Value { get => GetValue(ValueProperty); set => SetValue(ValueProperty, value); @@ -120,35 +146,37 @@ public object Value /// /// Allows the user to override the property that will be used as the value to validate. This is a bindable property. /// - public string ValuePropertyName + public string? ValuePropertyName { - get => (string)GetValue(ValuePropertyNameProperty); + get => (string?)GetValue(ValuePropertyNameProperty); set => SetValue(ValuePropertyNameProperty, value); } /// /// Allows the user to provide a custom that handles forcing validation. This is a bindable property. /// - public ICommand ForceValidateCommand + public ICommand? ForceValidateCommand { - get => (ICommand)GetValue(ForceValidateCommandProperty); + get => (ICommand?)GetValue(ForceValidateCommandProperty); set => SetValue(ForceValidateCommandProperty, value); } protected virtual string DefaultValuePropertyName => Entry.TextProperty.PropertyName; - protected virtual ICommand DefaultForceValidateCommand => new Command(ForceValidate); + protected virtual ICommand DefaultForceValidateCommand { get; } /// /// Forces the behavior to make a validation pass. /// - public void ForceValidate() => UpdateState(true); + public ValueTask ForceValidate() => UpdateStateAsync(true); - protected virtual object DecorateValue() => Value; + internal ValueTask ValidateNestedAsync(CancellationToken token) => UpdateStateAsync(true, token); - protected abstract bool Validate(object value); + protected virtual object? Decorate(object? value) => value; - protected override void OnAttachedTo(VisualElement bindable) + protected abstract ValueTask ValidateAsync(object? value, CancellationToken token); + + protected override async void OnAttachedTo(VisualElement bindable) { base.OnAttachedTo(bindable); @@ -156,7 +184,7 @@ protected override void OnAttachedTo(VisualElement bindable) currentStatus = ValidationFlags.ValidateOnAttaching; OnValuePropertyNamePropertyChanged(); - UpdateState(false); + await UpdateStateAsync(false); isAttaching = false; } @@ -172,20 +200,20 @@ protected override void OnDetachingFrom(VisualElement bindable) base.OnDetachingFrom(bindable); } - protected override void OnViewPropertyChanged(object sender, PropertyChangedEventArgs e) + protected override async void OnViewPropertyChanged(object? sender, PropertyChangedEventArgs e) { base.OnViewPropertyChanged(sender, e); if (e.PropertyName == VisualElement.IsFocusedProperty.PropertyName) { - currentStatus = View.IsFocused + currentStatus = View?.IsFocused is true ? ValidationFlags.ValidateOnFocusing : ValidationFlags.ValidateOnUnfocusing; - UpdateState(false); + await UpdateStateAsync(false); } } - protected static void OnValidationPropertyChanged(BindableObject bindable, object oldValue, object newValue) - => ((ValidationBehavior)bindable).UpdateState(false); + protected static async void OnValidationPropertyChanged(BindableObject bindable, object oldValue, object newValue) + => await ((ValidationBehavior)bindable).UpdateStateAsync(false); static void OnIsValidPropertyChanged(BindableObject bindable, object oldValue, object newValue) => ((ValidationBehavior)bindable).OnIsValidPropertyChanged(); @@ -232,24 +260,59 @@ void OnValuePropertyNamePropertyChanged() SetBinding(ValueProperty, defaultValueBinding); } - void UpdateState(bool isForced) + async ValueTask UpdateStateAsync(bool isForced, CancellationToken? parentToken = null) { if ((View?.IsFocused ?? false) && Flags.HasFlag(ValidationFlags.ForceMakeValidWhenFocused)) + { + IsRunning = true; + ResetValidationTokenSource(null); IsValid = true; + IsRunning = false; + } else if (isForced || (currentStatus != ValidationFlags.None && Flags.HasFlag(currentStatus))) - IsValid = Validate(DecorateValue()); + { + IsRunning = true; + using var tokenSource = new CancellationTokenSource(); + var token = parentToken ?? tokenSource.Token; + ResetValidationTokenSource(tokenSource); + + try + { + var isValid = await ValidateAsync(Decorate(Value), token).ConfigureAwait(false); + + if (token.IsCancellationRequested) + return; + + validationTokenSource = null; + IsValid = isValid; + IsRunning = false; + } + catch (TaskCanceledException) + { + return; + } + } UpdateStyle(); } void UpdateStyle() { - if (View == null || (ValidStyle ?? InvalidStyle) == null) + if (View == null) return; - View.Style = IsValid - ? ValidStyle - : InvalidStyle; + VisualStateManager.GoToState(View, IsValid ? ValidVisualState : InvalidVisualState); + + if ((ValidStyle ?? InvalidStyle) == null) + return; + + View.Style = IsValid ? ValidStyle : InvalidStyle; + } + + void ResetValidationTokenSource(CancellationTokenSource? newTokenSource) + { + validationTokenSource?.Cancel(); + validationTokenSource = newTokenSource; } } -} \ No newline at end of file +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/BoolToObjectConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/BoolToObjectConverter.shared.cs index 3cde18df6..4ce274c39 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/BoolToObjectConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/BoolToObjectConverter.shared.cs @@ -20,12 +20,12 @@ public class BoolToObjectConverter : ValueConverterExtension, IValueCon /// /// The object that corresponds to True value. /// - public TObject TrueObject { get; set; } + public TObject? TrueObject { get; set; } /// /// The object that corresponds to False value. /// - public TObject FalseObject { get; set; } + public TObject? FalseObject { get; set; } /// /// Converts to object. @@ -35,7 +35,7 @@ public class BoolToObjectConverter : ValueConverterExtension, IValueCon /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// The object assigned to if value equals True, otherwise the value assigned to . - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object? Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) { if (value is bool result) return result ? TrueObject : FalseObject; @@ -51,7 +51,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// True if value equals , otherwise False. - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) { if (value is TObject result) return result.Equals(TrueObject); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ByteArrayToImageSourceConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ByteArrayToImageSourceConverter.shared.cs index 19042194c..2df999c12 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ByteArrayToImageSourceConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ByteArrayToImageSourceConverter.shared.cs @@ -20,9 +20,9 @@ public class ByteArrayToImageSourceConverter : ValueConverterExtension, IValueCo /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// An object of type . - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object? Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) { - if (value is null) + if (value == null) return null; if (value is byte[] imageBytes) @@ -39,9 +39,9 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// An object of type . - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object? ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) { - if (value is null) + if (value == null) return null; if (value is StreamImageSource streamImageSource) diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/DateTimeOffsetConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/DateTimeOffsetConverter.shared.cs index c16ec6331..261be88fc 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/DateTimeOffsetConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/DateTimeOffsetConverter.shared.cs @@ -17,7 +17,7 @@ public class DateTimeOffsetConverter : ValueConverterExtension, IValueConverter /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// The value. - public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + public object Convert(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture) => value is DateTimeOffset dateTimeOffset ? dateTimeOffset.DateTime : throw new ArgumentException("Value is not a valid DateTimeOffset", nameof(value)); @@ -30,7 +30,7 @@ public object Convert(object value, Type targetType, object parameter, System.Gl /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented.. /// The value. - public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + public object ConvertBack(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture) => value is DateTime dateTime ? dateTime.Kind switch { diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/DoubleToIntConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/DoubleToIntConverter.shared.cs index 9697ddea0..ce189f125 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/DoubleToIntConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/DoubleToIntConverter.shared.cs @@ -24,7 +24,7 @@ public class DoubleToIntConverter : ValueConverterExtension, IValueConverter /// Multiplier (Equals 1 by default). /// The culture to use in the converter. This is not implemented. /// value. - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) => value is double result ? (int)Math.Round(result * GetParameter(parameter)) : throw new ArgumentException("Value is not a valid double", nameof(value)); @@ -37,12 +37,12 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// Denominator (Equals 1 by default). /// The culture to use in the converter. This is not implemented. /// value. - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) => value is int result ? result / GetParameter(parameter) : throw new ArgumentException("Value is not a valid integer", nameof(value)); - double GetParameter(object parameter) + double GetParameter(object? parameter) => parameter == null ? Ratio : parameter switch diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/EnumToBoolConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/EnumToBoolConverter.shared.cs new file mode 100644 index 000000000..803ffad87 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/EnumToBoolConverter.shared.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Xamarin.CommunityToolkit.Extensions.Internals; +using System.Reflection; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.Converters +{ + /// + /// Convert an to corresponding + /// + public class EnumToBoolConverter : ValueConverterExtension, IValueConverter + { + /// + /// Enum values, that converts to true (optional) + /// + public IList TrueValues { get; } = new List(); + + /// + /// Convert an to corresponding + /// + /// value to convert + /// The type of the binding target property. This is not implemented. + /// + /// Additional parameter for converter. Can be used for comparison instead of + /// + /// + /// The culture to use in the converter. This is not implemented. + /// + /// False, if the value is not in . False, if is empty and + /// value not equal to parameter. + /// + /// If value is not an + public object Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) + { + if (value is not Enum enumValue) + throw new ArgumentException("The value should be of type Enum", nameof(value)); + + return TrueValues.Count == 0 + ? CompareTwoEnums(enumValue, parameter as Enum) + : TrueValues.Any(item => CompareTwoEnums(enumValue, item)); + + static bool CompareTwoEnums(Enum valueToCheck, object? referenceValue) + { + if (referenceValue is not Enum referenceEnumValue) + return false; + + var valueToCheckType = valueToCheck.GetType(); + if (valueToCheckType != referenceEnumValue.GetType()) + return false; + + if (valueToCheckType.GetTypeInfo().GetCustomAttribute() != null) + return referenceEnumValue.HasFlag(valueToCheck); + + return Equals(valueToCheck, referenceEnumValue); + } + } + + /// + public object ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) => + throw new NotImplementedException(); + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/EqualConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/EqualConverter.shared.cs index 7996ec1be..4721bc7eb 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/EqualConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/EqualConverter.shared.cs @@ -18,9 +18,9 @@ public class EqualConverter : ValueConverterExtension, IValueConverter /// The second object to compare. /// The culture to use in the converter. This is not implemented. /// True if and are equal, False if they are not equal. - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => ConvertInternal(value, parameter); + public object Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) => ConvertInternal(value, parameter); - internal static bool ConvertInternal(object value, object parameter) => + internal static bool ConvertInternal(object? value, object? parameter) => (value != null && value.Equals(parameter)) || (value == null && parameter == null); /// @@ -31,7 +31,7 @@ public class EqualConverter : ValueConverterExtension, IValueConverter /// N/A /// N/A /// N/A - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => - throw new NotImplementedException(); + public object ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) + => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ImageResourceConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ImageResourceConverter.shared.cs new file mode 100644 index 000000000..009925703 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ImageResourceConverter.shared.cs @@ -0,0 +1,40 @@ +using System; +using System.Globalization; +using Xamarin.Forms; +#if NETSTANDARD1_0 || UAP10_0 +using System.Reflection; +#endif + +namespace Xamarin.CommunityToolkit.Converters +{ + /// + /// Converts embedded image resource ID to it ImageSource. + /// + public class ImageResourceConverter : IValueConverter + { + /// + /// Converts embedded image resource ID to it ImageSource. + /// + /// The value to convert. + /// The type of the binding target property. This is not implemented. + /// Additional parameter for the converter to handle. This is not implemented. + /// The culture to use in the converter. This is not implemented. + /// The ImageSource related to the provided resource ID of the embedded image. If it's null it will returns null. + public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value == null) + return null; + + if (value is not string imageId) + throw new ArgumentException("Value is not a string", nameof(value)); + + return ImageSource.FromResource(imageId, Application.Current.GetType() +#if NETSTANDARD1_0 || UAP10_0 + .GetTypeInfo() +#endif + .Assembly); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => throw new NotImplementedException(); + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/IndexToArrayItemConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/IndexToArrayItemConverter.shared.cs index 3fd9a0e24..7f600b5ca 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/IndexToArrayItemConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/IndexToArrayItemConverter.shared.cs @@ -18,12 +18,12 @@ public class IndexToArrayItemConverter : ValueConverterExtension, IValueConverte /// The items array. /// The culture to use in the converter. This is not implemented. /// The item from the array that corresponds to passed index. - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object? Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) { - if (!(value is int index)) + if (value is not int index) throw new ArgumentException("Value is not a valid integer", nameof(value)); - if (!(parameter is Array array)) + if (parameter is not Array array) throw new ArgumentException("Parameter is not a valid array", nameof(parameter)); if (index < 0 || index >= array.Length) @@ -40,9 +40,9 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// The items array. /// The culture to use in the converter. This is not implemented. /// The index of the item from the array. - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object? ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) { - if (!(parameter is Array array)) + if (parameter is not Array array) throw new ArgumentException("Parameter is not a valid array", nameof(parameter)); for (var i = 0; i < array.Length; i++) diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/IntToBoolConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/IntToBoolConverter.shared.cs index 453df7906..58de3abde 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/IntToBoolConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/IntToBoolConverter.shared.cs @@ -18,7 +18,7 @@ public class IntToBoolConverter : ValueConverterExtension, IValueConverter /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// False if the value is 0, otherwise if the value is anything but 0 it returns True. - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) => value is int result ? result != 0 : throw new ArgumentException("Value is not a valid integer", nameof(value)); @@ -31,7 +31,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// 0 if the value is False, otherwise 1 if the value is True. - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) { if (value is bool result) return result ? 1 : 0; diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/InvertedBoolConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/InvertedBoolConverter.shared.cs index 66ff4e97e..7991605ef 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/InvertedBoolConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/InvertedBoolConverter.shared.cs @@ -18,7 +18,7 @@ public class InvertedBoolConverter : ValueConverterExtension, IValueConverter /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// An inverted from the one coming in. - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) => InverseBool(value); /// @@ -29,7 +29,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// An inverted from the one coming in. - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) => InverseBool(value); /// @@ -37,7 +37,7 @@ public object ConvertBack(object value, Type targetType, object parameter, Cultu /// /// The value to inverse. /// The inverted value of the incoming . - bool InverseBool(object value) + bool InverseBool(object? value) { if (value is bool result) return !result; diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/IsNotNullOrEmptyConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/IsNotNullOrEmptyConverter.shared.cs index ec3c45eab..c36d149b6 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/IsNotNullOrEmptyConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/IsNotNullOrEmptyConverter.shared.cs @@ -18,7 +18,7 @@ public class IsNotNullOrEmptyConverter : ValueConverterExtension, IValueConverte /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// A indicating if the incoming value is not null and not empty. - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => + public object Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) => !IsNullOrEmptyConverter.ConvertInternal(value); /// @@ -29,7 +29,7 @@ public class IsNotNullOrEmptyConverter : ValueConverterExtension, IValueConverte /// N/A /// N/A /// N/A - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object? ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/IsNullOrEmptyConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/IsNullOrEmptyConverter.shared.cs index 8d195b934..fcca65864 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/IsNullOrEmptyConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/IsNullOrEmptyConverter.shared.cs @@ -18,9 +18,9 @@ public class IsNullOrEmptyConverter : ValueConverterExtension, IValueConverter /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// A indicating if the incoming value is null or empty. - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => ConvertInternal(value); + public object Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) => ConvertInternal(value); - internal static bool ConvertInternal(object value) => + internal static bool ConvertInternal(object? value) => value == null || (value is string str && string.IsNullOrWhiteSpace(str)); /// @@ -31,7 +31,7 @@ public class IsNullOrEmptyConverter : ValueConverterExtension, IValueConverter /// N/A /// N/A /// N/A - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ItemSelectedEventArgsConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ItemSelectedEventArgsConverter.shared.cs index f1a48906b..7de63b824 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ItemSelectedEventArgsConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ItemSelectedEventArgsConverter.shared.cs @@ -18,7 +18,7 @@ public class ItemSelectedEventArgsConverter : ValueConverterExtension, IValueCon /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// A object from object of type . - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object? Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) { if (value == null) return null; @@ -36,7 +36,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// N/A /// N/A /// N/A - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object? ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ItemTappedEventArgsConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ItemTappedEventArgsConverter.shared.cs index 83bd96497..800a0c4d6 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ItemTappedEventArgsConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ItemTappedEventArgsConverter.shared.cs @@ -18,7 +18,7 @@ public class ItemTappedEventArgsConverter : ValueConverterExtension, IValueConve /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// A object from object of type . - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object? Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) { if (value == null) return null; @@ -36,7 +36,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// N/A /// N/A /// N/A - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object? ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ListIsNotNullOrEmptyConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ListIsNotNullOrEmptyConverter.shared.cs index 109426043..a7a6db1b3 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ListIsNotNullOrEmptyConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ListIsNotNullOrEmptyConverter.shared.cs @@ -18,7 +18,7 @@ public class ListIsNotNullOrEmptyConverter : ValueConverterExtension, IValueConv /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// A indicating if the incoming value is not null and not empty. - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => + public object Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) => !ListIsNullOrEmptyConverter.ConvertInternal(value); /// @@ -29,7 +29,7 @@ public class ListIsNotNullOrEmptyConverter : ValueConverterExtension, IValueConv /// N/A /// N/A /// N/A - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ListIsNullOrEmptyConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ListIsNullOrEmptyConverter.shared.cs index 52f2630fb..0a09cc125 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ListIsNullOrEmptyConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ListIsNullOrEmptyConverter.shared.cs @@ -19,11 +19,11 @@ public class ListIsNullOrEmptyConverter : ValueConverterExtension, IValueConvert /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// A indicating if the incoming value is null or empty. - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => ConvertInternal(value); + public object Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) => ConvertInternal(value); - internal static bool ConvertInternal(object value) + internal static bool ConvertInternal(object? value) { - if (value is null) + if (value == null) return true; if (value is IEnumerable list) @@ -40,7 +40,7 @@ internal static bool ConvertInternal(object value) /// N/A /// N/A /// N/A - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object? ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ListToStringConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ListToStringConverter.shared.cs index 85c7d3bad..8144c5832 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ListToStringConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/ListToStringConverter.shared.cs @@ -15,7 +15,7 @@ public class ListToStringConverter : ValueConverterExtension, IValueConverter /// /// The separator that should be between each item in the collection /// - public string Separator { get; set; } + public string Separator { get; set; } = string.Empty; /// /// Concatenates the items of a collection, using the specified between each item. On each item ToString() will be called. @@ -25,15 +25,15 @@ public class ListToStringConverter : ValueConverterExtension, IValueConverter /// The separator that should be between each collection item. This overrides the value in . /// The culture to use in the converter. This is not implemented. /// Concatenated members string separated by or, if set, . - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object? Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) { if (value == null) return string.Empty; - if (!(value is IEnumerable enumerable)) + if (value is not IEnumerable enumerable) throw new ArgumentException("Value cannot be casted to IEnumerable", nameof(value)); - if (!((parameter ?? Separator ?? string.Empty) is string separator)) + if ((parameter ?? Separator ?? string.Empty) is not string separator) throw new ArgumentException("Parameter cannot be casted to string", nameof(parameter)); var collection = enumerable @@ -52,7 +52,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// N/A /// N/A /// N/A - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object? ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/MultiConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/MultiConverter.shared.cs index f4d406ffe..b6d8faacf 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/MultiConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/MultiConverter.shared.cs @@ -18,7 +18,7 @@ public class MultiConverter : List, IValueConverter /// Parameter to pass into subsequent converters. /// The culture to use in the converter. /// The converted value. - public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + public object? Convert(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture) => parameter is IList parameters ? this.Aggregate(value, (current, converter) => converter.Convert(current, targetType, parameters.FirstOrDefault(x => x.ConverterType == converter.GetType())?.Value, culture)) @@ -32,7 +32,7 @@ public object Convert(object value, Type targetType, object parameter, System.Gl /// N/A /// N/A /// N/A - public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + public object? ConvertBack(object? value, Type targetType, object? parameter, System.Globalization.CultureInfo culture) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/MultiConverterParameter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/MultiConverterParameter.shared.cs index 85950c23d..0ff557592 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/MultiConverterParameter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/MultiConverterParameter.shared.cs @@ -11,11 +11,11 @@ public class MultiConverterParameter : BindableObject /// /// The type of object of this parameter. /// - public Type ConverterType { get; set; } + public Type? ConverterType { get; set; } /// /// The value of this parameter. /// - public object Value { get; set; } + public object? Value { get; set; } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/NotEqualConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/NotEqualConverter.shared.cs index 30027df8d..3d798a2af 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/NotEqualConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/NotEqualConverter.shared.cs @@ -18,7 +18,7 @@ public class NotEqualConverter : ValueConverterExtension, IValueConverter /// The second object to compare. /// The culture to use in the converter. This is not implemented. /// True if and are not equal, False if they are equal. - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) => + public object Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) => !EqualConverter.ConvertInternal(value, parameter); /// @@ -29,7 +29,7 @@ public class NotEqualConverter : ValueConverterExtension, IValueConverter /// N/A /// N/A /// N/A - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) => throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/StateToBooleanConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/StateToBooleanConverter.shared.cs index 21fe1fea0..9a1ea89c4 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/StateToBooleanConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/StateToBooleanConverter.shared.cs @@ -23,9 +23,9 @@ public class StateToBooleanConverter : IValueConverter /// Optionally, a can be supplied here to compare against. /// The culture to use in the converter. This is not implemented. /// True if the provided s match, otherwise False if they don't match. - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) { - if (!(value is LayoutState state)) + if (value is not LayoutState state) throw new ArgumentException("Value is not a valid State", nameof(value)); if (parameter is LayoutState stateToCompare) @@ -42,7 +42,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// N/A /// N/A /// N/A - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) +public object ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) => throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TextCaseConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TextCaseConverter.shared.cs index 41975112f..1e28938b2 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TextCaseConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TextCaseConverter.shared.cs @@ -24,10 +24,20 @@ public class TextCaseConverter : ValueConverterExtension, IValueConverter /// The desired text case that the text should be converted to. Must match enum value. /// The culture to use in the converter. This is not implemented. /// The converted text representation with the desired casing. - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - => value == null || value is string || value is char - ? Convert(value?.ToString(), parameter) - : throw new ArgumentException("Value is neither a string nor a char", nameof(value)); + public object? Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) + { + var str = value?.ToString(); + if (str == null || string.IsNullOrWhiteSpace(str)) + return str; + + return GetParameter(parameter) switch + { + TextCaseType.Lower => str.ToLowerInvariant(), + TextCaseType.Upper => str.ToUpperInvariant(), + TextCaseType.FirstUpperRestLower => str.Substring(0, 1).ToUpperInvariant() + str.ToString().Substring(1).ToLowerInvariant(), + _ => str + }; + } /// /// This method is not implemented and will throw a . @@ -37,30 +47,19 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// N/A /// N/A /// N/A - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) => throw new NotImplementedException(); - object Convert(string value, object parameter) - => GetParameter(parameter) switch - { - TextCaseType.Lower => value?.ToLowerInvariant(), - TextCaseType.Upper => value?.ToUpperInvariant(), - _ => value - }; - - TextCaseType GetParameter(object parameter) - => parameter == null - ? Type - : parameter switch - { - TextCaseType type => type, - string typeString => Enum.TryParse(typeString, out TextCaseType result) - ? result - : throw new ArgumentException("Cannot parse text case from the string", nameof(parameter)), - int typeInt => Enum.IsDefined(typeof(TextCaseType), typeInt) - ? (TextCaseType)typeInt - : throw new ArgumentException("Cannot convert integer to text case enum value", nameof(parameter)), - _ => TextCaseType.None, - }; + TextCaseType GetParameter(object? parameter) => parameter switch + { + TextCaseType type => type, + string typeString => Enum.TryParse(typeString, out TextCaseType result) + ? result + : throw new ArgumentException("Cannot parse text case from the string", nameof(parameter)), + int typeInt => Enum.IsDefined(typeof(TextCaseType), typeInt) + ? (TextCaseType)typeInt + : throw new ArgumentException("Cannot convert integer to text case enum value", nameof(parameter)), + _ => Type, + }; } -} \ No newline at end of file +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TextCaseType.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TextCaseType.shared.cs index 0e67c1328..0c4e9aebd 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TextCaseType.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TextCaseType.shared.cs @@ -13,5 +13,8 @@ public enum TextCaseType /// Convert to lowercase Lower, + + /// Converts the first letter to upper only + FirstUpperRestLower, } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TimeSpanToDoubleConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TimeSpanToDoubleConverter.shared.cs index 14d026cf9..fa1363495 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TimeSpanToDoubleConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/TimeSpanToDoubleConverter.shared.cs @@ -17,7 +17,7 @@ public class TimeSpanToDoubleConverter : IValueConverter /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// A value expressed in seconds. - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object Convert(object? value, Type? targetType, object? parameter, CultureInfo? culture) { if (value is TimeSpan timespan) { @@ -35,7 +35,7 @@ public object Convert(object value, Type targetType, object parameter, CultureIn /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// The value representing the converted value. - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object ConvertBack(object? value, Type? targetType, object? parameter, CultureInfo? culture) { if (value is double doubleValue) { diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/VariableMultiValueConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/VariableMultiValueConverter.shared.cs index ffc7afb09..0d32f0719 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/VariableMultiValueConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Converters/VariableMultiValueConverter.shared.cs @@ -31,7 +31,7 @@ public class VariableMultiValueConverter : MultiValueConverterExtension, IMultiV /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// A single value dependant on the configuration for this converter. - public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + public object Convert(object[]? values, Type targetType, object? parameter, CultureInfo culture) { if (values == null || values.Length == 0) return false; @@ -62,9 +62,9 @@ public object Convert(object[] values, Type targetType, object parameter, Cultur /// Additional parameter for the converter to handle. This is not implemented. /// The culture to use in the converter. This is not implemented. /// All bindings that evaluate to true if is true. Or null if is not a value or is false. - public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) + public object[]? ConvertBack(object? value, Type[] targetTypes, object? parameter, CultureInfo culture) { - if (!(value is bool boolValue) || targetTypes.Any(t => !t.IsAssignableFrom(typeof(bool)))) + if (value is not bool boolValue || targetTypes.Any(t => !t.IsAssignableFrom(typeof(bool)))) return null; return boolValue ? targetTypes.Select(t => ConditionType == MultiBindingCondition.All).OfType().ToArray() : null; diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/FileMediaSource.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/FileMediaSource.shared.cs index 49edbc3a6..e875603c4 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/FileMediaSource.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/FileMediaSource.shared.cs @@ -9,9 +9,9 @@ public sealed class FileMediaSource : MediaSource public static readonly BindableProperty FileProperty = BindableProperty.Create(nameof(File), typeof(string), typeof(FileMediaSource), propertyChanged: OnFileMediaSourceChanged); - public string File + public string? File { - get => (string)GetValue(FileProperty); + get => (string?)GetValue(FileProperty); set => SetValue(FileProperty, value); } @@ -19,7 +19,7 @@ public string File public static implicit operator FileMediaSource(string file) => (FileMediaSource)FromFile(file); - public static implicit operator string(FileMediaSource file) => file?.File; + public static implicit operator string?(FileMediaSource? file) => file?.File; static void OnFileMediaSourceChanged(BindableObject bindable, object oldValue, object newValue) => ((FileMediaSource)bindable).OnSourceChanged(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/MediaSource.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/MediaSource.shared.cs index cba24f769..adc286b8e 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/MediaSource.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/MediaSource.shared.cs @@ -10,22 +10,27 @@ public abstract class MediaSource : Element { readonly WeakEventManager weakEventManager = new WeakEventManager(); - public static MediaSource FromFile(string file) => + public static MediaSource FromFile(string? file) => new FileMediaSource { File = file }; - public static MediaSource FromUri(Uri uri) => - !uri.IsAbsoluteUri ? throw new ArgumentException("Uri must be be absolute", nameof(uri)) : new UriMediaSource { Uri = uri }; + public static MediaSource? FromUri(Uri? uri) + { + if (uri == null) + return null; + + return !uri.IsAbsoluteUri ? throw new ArgumentException("Uri must be be absolute", nameof(uri)) : new UriMediaSource { Uri = uri }; + } - public static MediaSource FromUri(string uri) => FromUri(new Uri(uri)); + public static MediaSource? FromUri(string uri) => FromUri(new Uri(uri)); [Preserve(Conditional = true)] - public static implicit operator MediaSource(string source) => + public static implicit operator MediaSource?(string? source) => Uri.TryCreate(source, UriKind.Absolute, out var uri) && uri.Scheme != "file" ? FromUri(uri) : FromFile(source); [Preserve(Conditional = true)] - public static implicit operator MediaSource(Uri uri) => uri == null ? null : FromUri(uri); + public static implicit operator MediaSource?(Uri? uri) => FromUri(uri); protected void OnSourceChanged() => weakEventManager.RaiseEvent(this, EventArgs.Empty, nameof(SourceChanged)); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/MediaSourceConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/MediaSourceConverter.shared.cs index bf26d70e4..4881b7ae0 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/MediaSourceConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/MediaSourceConverter.shared.cs @@ -1,12 +1,11 @@ using System; using Xamarin.Forms; -using Xamarin.Forms.Internals; namespace Xamarin.CommunityToolkit.Core { public sealed class MediaSourceConverter : TypeConverter { - public override object ConvertFromInvariantString(string value) + public override object? ConvertFromInvariantString(string value) { if (value == null) throw new InvalidOperationException($"Cannot convert \"{value}\" into {typeof(MediaSource)}"); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/StreamMediaSource.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/StreamMediaSource.shared.cs index 37e2d41b9..a24072909 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/StreamMediaSource.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/StreamMediaSource.shared.cs @@ -9,14 +9,14 @@ namespace Xamarin.CommunityToolkit.Core public class StreamMediaSource : MediaSource, IStreamImageSource { readonly object synchandle = new object(); - CancellationTokenSource cancellationTokenSource; + CancellationTokenSource? cancellationTokenSource; - TaskCompletionSource completionSource; + TaskCompletionSource? completionSource; public static readonly BindableProperty StreamProperty = BindableProperty.Create(nameof(Stream), typeof(Func>), typeof(StreamMediaSource)); - protected CancellationTokenSource CancellationTokenSource + protected CancellationTokenSource? CancellationTokenSource { get => cancellationTokenSource; private set @@ -34,9 +34,9 @@ private set bool IsLoading => cancellationTokenSource != null; - public virtual Func> Stream + public virtual Func>? Stream { - get => (Func>)GetValue(StreamProperty); + get => (Func>?)GetValue(StreamProperty); set => SetValue(StreamProperty, value); } @@ -47,12 +47,16 @@ protected override void OnPropertyChanged(string propertyName) base.OnPropertyChanged(propertyName); } - async Task IStreamImageSource.GetStreamAsync(CancellationToken userToken) + async Task IStreamImageSource.GetStreamAsync(CancellationToken userToken) { if (Stream == null) return null; OnLoadingStarted(); + + if (CancellationTokenSource == null) + throw new InvalidOperationException($"{nameof(OnLoadingStarted)} not called"); + userToken.Register(CancellationTokenSource.Cancel); try { @@ -72,7 +76,7 @@ protected void OnLoadingCompleted(bool cancelled) if (!IsLoading || completionSource == null) return; - var tcs = Interlocked.Exchange(ref completionSource, null); + var tcs = Interlocked.Exchange?>(ref completionSource, null); if (tcs != null) tcs.SetResult(cancelled); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/UriMediaSource.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/UriMediaSource.shared.cs index 38910d4ff..8751695fb 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/UriMediaSource.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Core/UriMediaSource.shared.cs @@ -15,9 +15,9 @@ public sealed class UriMediaSource : MediaSource ((UriMediaSource)bindable).OnSourceChanged(); [TypeConverter(typeof(Xamarin.Forms.UriTypeConverter))] - public Uri Uri + public Uri? Uri { - get => (Uri)GetValue(UriProperty); + get => (Uri?)GetValue(UriProperty); set => SetValue(UriProperty, value); } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/EffectIds.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/EffectIds.shared.cs index 9b88e2b10..f86174aaf 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/EffectIds.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/EffectIds.shared.cs @@ -40,5 +40,15 @@ sealed class EffectIds /// Effect Id for /// public static string TouchEffect => $"{effectResolutionGroupName}.{nameof(TouchEffect)}"; + + /// + /// Effect Id for + /// + public static string LifeCycleEffect => $"{effectResolutionGroupName}.{nameof(LifecycleEffect)}"; + + /// + /// Effect Id for + /// + public static string ShadowEffect => $"{effectResolutionGroupName}.{nameof(ShadowEffect)}"; } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/IconTintColor/IconTintColorEffectRouter.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/IconTintColor/IconTintColorEffectRouter.android.cs index 4d7a5e05e..51358429e 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/IconTintColor/IconTintColorEffectRouter.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/IconTintColor/IconTintColorEffectRouter.android.cs @@ -4,6 +4,7 @@ using Android.Graphics; using Android.Widget; using Xamarin.CommunityToolkit.Effects; +using Xamarin.CommunityToolkit.Extensions; using Xamarin.Forms.Platform.Android; using Effects = Xamarin.CommunityToolkit.Android.Effects; @@ -33,8 +34,10 @@ protected override void OnElementPropertyChanged(PropertyChangedEventArgs args) void ApplyTintColor() { - if (Control == null || Element == null) + if (!Control.IsAlive() || Element == null) + { return; + } var color = IconTintColorEffect.GetTintColor(Element); @@ -51,21 +54,21 @@ void ApplyTintColor() void ClearTintColor() { - try + // Because of a XF bug: https://github.com/xamarin/Xamarin.Forms/issues/13889 + if (!Control.IsAlive()) { - switch (Control) - { - case ImageView image: - image.ClearColorFilter(); - break; - case Button button: - foreach (var drawable in button.GetCompoundDrawables()) - drawable?.ClearColorFilter(); - break; - } + return; } - catch (ObjectDisposedException) { - // We ignore ObjectDisposedException as a workaround of XF issue https://github.com/xamarin/Xamarin.Forms/issues/13889 + + switch (Control) + { + case ImageView image: + image.ClearColorFilter(); + break; + case Button button: + foreach (var drawable in button.GetCompoundDrawables()) + drawable?.ClearColorFilter(); + break; } } @@ -74,7 +77,7 @@ void SetImageViewTintColor(ImageView image, Forms.Color color) if (color == Forms.Color.Default) image.ClearColorFilter(); - image.SetColorFilter(new PorterDuffColorFilter(color.ToAndroid(), PorterDuff.Mode.SrcIn)); + image.SetColorFilter(new PorterDuffColorFilter(color.ToAndroid(), PorterDuff.Mode.SrcIn ?? throw new NullReferenceException())); } void SetButtonTintColor(Button button, Forms.Color color) diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/LifeCycle/LifeCycleEffect.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/LifeCycle/LifeCycleEffect.shared.cs new file mode 100644 index 000000000..d182f4267 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/LifeCycle/LifeCycleEffect.shared.cs @@ -0,0 +1,54 @@ +using System; +using Xamarin.CommunityToolkit.Helpers; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.Effects +{ + /// + /// An effect to subscribe to the View's lifecycle events. + /// + public class LifecycleEffect : RoutingEffect + { + readonly WeakEventManager eventManager = new WeakEventManager(); + + /// + /// Event that is triggered when the is loaded and is ready for use. + /// + public event EventHandler Loaded + { + add => eventManager.AddEventHandler(value); + remove => eventManager.RemoveEventHandler(value); + } + + /// + /// Event that is triggered when the is unloaded and isn't ready for use. + /// + public event EventHandler Unloaded + { + add => eventManager.AddEventHandler(value); + remove => eventManager.RemoveEventHandler(value); + } + + /// + /// Constructor for the + /// + public LifecycleEffect() + : base(EffectIds.LifeCycleEffect) + { +#if __ANDROID__ + if (System.DateTime.Now.Ticks < 0) + _ = new Xamarin.CommunityToolkit.Android.Effects.LifeCycleEffectRouter(); +#elif __IOS__ + if (System.DateTime.Now.Ticks < 0) + _ = new Xamarin.CommunityToolkit.iOS.Effects.LifeCycleEffectRouter(); +#elif UWP + if (System.DateTime.Now.Ticks < 0) + _ = new Xamarin.CommunityToolkit.UWP.Effects.LifeCycleEffectRouter(); +#endif + } + + internal void RaiseLoadedEvent(Element element) => eventManager.RaiseEvent(element, EventArgs.Empty, nameof(Loaded)); + + internal void RaiseUnloadedEvent(Element element) => eventManager.RaiseEvent(element, EventArgs.Empty, nameof(Unloaded)); + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/LifeCycle/LifeCycleEffectRouter.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/LifeCycle/LifeCycleEffectRouter.android.cs new file mode 100644 index 000000000..85c6e4d94 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/LifeCycle/LifeCycleEffectRouter.android.cs @@ -0,0 +1,53 @@ +using System; +using System.Linq; +using Xamarin.CommunityToolkit.Android.Effects; +using Xamarin.CommunityToolkit.Effects; +using Xamarin.Forms; +using Xamarin.Forms.Platform.Android; +using View = Android.Views.View; + +[assembly: ExportEffect(typeof(LifeCycleEffectRouter), nameof(LifecycleEffect))] + +namespace Xamarin.CommunityToolkit.Android.Effects +{ + /// + /// Android implementation of the + /// + public class LifeCycleEffectRouter : PlatformEffect + { + View? nativeView; + LifecycleEffect? lifeCycleEffect; + + protected override void OnAttached() + { + lifeCycleEffect = Element.Effects.OfType().FirstOrDefault() ?? + throw new ArgumentNullException($"The effect {nameof(LifecycleEffect)} can't be null."); + + nativeView = Control ?? Container; + + nativeView.ViewAttachedToWindow += OnNativeViewViewAttachedToWindow; + nativeView.ViewDetachedFromWindow += OnNativeViewViewDetachedFromWindow; + } + + void OnNativeViewViewAttachedToWindow(object? sender, View.ViewAttachedToWindowEventArgs e) => lifeCycleEffect?.RaiseLoadedEvent(Element); + + void OnNativeViewViewDetachedFromWindow(object? sender, View.ViewDetachedFromWindowEventArgs e) + { + if (lifeCycleEffect != null) + lifeCycleEffect.RaiseUnloadedEvent(Element); + + if (nativeView != null) + { + nativeView.ViewDetachedFromWindow -= OnNativeViewViewDetachedFromWindow; + nativeView.ViewAttachedToWindow -= OnNativeViewViewAttachedToWindow; + } + + nativeView = null; + lifeCycleEffect = null; + } + + protected override void OnDetached() + { + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/LifeCycle/LifeCycleEffectRouter.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/LifeCycle/LifeCycleEffectRouter.ios.cs new file mode 100644 index 000000000..4eaa543c7 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/LifeCycle/LifeCycleEffectRouter.ios.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using Xamarin.CommunityToolkit.Effects; +using Xamarin.CommunityToolkit.iOS.Effects; +using Xamarin.Forms; +using Xamarin.Forms.Platform.iOS; + +[assembly: ExportEffect(typeof(LifeCycleEffectRouter), nameof(LifecycleEffect))] + +namespace Xamarin.CommunityToolkit.iOS.Effects +{ + /// + /// iOS implementation of the + /// + public class LifeCycleEffectRouter : PlatformEffect + { + LifecycleEffect? lifeCycleEffect; + + protected override void OnAttached() + { + lifeCycleEffect = Element.Effects.OfType().FirstOrDefault() ?? + throw new ArgumentNullException($"The effect {nameof(LifecycleEffect)} can't be null."); + + Element.PropertyChanged += OnPropertyChanged; + } + + void OnPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) + { + if (e.PropertyName == "Renderer" && lifeCycleEffect != null) + { + var result = Platform.GetRenderer((VisualElement)Element); + + if (result != null) + lifeCycleEffect.RaiseLoadedEvent(Element); + else + { + lifeCycleEffect.RaiseUnloadedEvent(Element); + lifeCycleEffect = null; + Element.PropertyChanged -= OnPropertyChanged; + } + } + } + + protected override void OnDetached() + { + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/LifeCycle/LifeCycleEffectRouter.uwp.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/LifeCycle/LifeCycleEffectRouter.uwp.cs new file mode 100644 index 000000000..734ee1149 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/LifeCycle/LifeCycleEffectRouter.uwp.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using Windows.UI.Xaml; +using Xamarin.CommunityToolkit.Effects; +using Xamarin.CommunityToolkit.UWP.Effects; +using Xamarin.Forms; +using Xamarin.Forms.Platform.UWP; + +[assembly: ExportEffect(typeof(LifeCycleEffectRouter), nameof(LifecycleEffect))] + +namespace Xamarin.CommunityToolkit.UWP.Effects +{ + /// + /// UWP implementation of the + /// + public class LifeCycleEffectRouter : PlatformEffect + { + FrameworkElement? nativeView; + LifecycleEffect? lifeCycleEffect; + + protected override void OnAttached() + { + lifeCycleEffect = Element.Effects.OfType().FirstOrDefault() ?? + throw new ArgumentNullException($"The effect {nameof(LifecycleEffect)} can't be null."); + + nativeView = Control ?? Container; + + nativeView.Loaded += OnNativeViewLoaded; + nativeView.Unloaded += OnNativeViewUnloaded; + } + + void OnNativeViewLoaded(object? sender, RoutedEventArgs e) => lifeCycleEffect?.RaiseLoadedEvent(Element); + + void OnNativeViewUnloaded(object? sender, RoutedEventArgs e) + { + if (lifeCycleEffect != null) + { + lifeCycleEffect.RaiseUnloadedEvent(Element); + } + + if (nativeView != null) + { + nativeView.Unloaded -= OnNativeViewUnloaded; + nativeView.Loaded -= OnNativeViewLoaded; + } + + lifeCycleEffect = null; + nativeView = null; + } + + protected override void OnDetached() + { + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/RemoveBorder/RemoveBorderEffect.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/RemoveBorder/RemoveBorderEffect.android.cs index af2759c2c..64174e120 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/RemoveBorder/RemoveBorderEffect.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/RemoveBorder/RemoveBorderEffect.android.cs @@ -1,7 +1,6 @@ using Android.Graphics; using Android.Graphics.Drawables; using Android.Graphics.Drawables.Shapes; -using Android.Runtime; using Xamarin.CommunityToolkit.Effects; using Xamarin.Forms; using Xamarin.Forms.Platform.Android; @@ -13,16 +12,20 @@ namespace Xamarin.CommunityToolkit.Android.Effects { public class RemoveBorderEffect : PlatformEffect { - Drawable originalBackground; + Drawable? originalBackground; protected override void OnAttached() { originalBackground = Control.Background; var shape = new ShapeDrawable(new RectShape()); - shape.Paint.Color = global::Android.Graphics.Color.Transparent; - shape.Paint.StrokeWidth = 0; - shape.Paint.SetStyle(Paint.Style.Stroke); + if (shape.Paint != null) + { + shape.Paint.Color = global::Android.Graphics.Color.Transparent; + shape.Paint.StrokeWidth = 0; + shape.Paint.SetStyle(Paint.Style.Stroke); + } + Control.Background = shape; } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/RemoveBorder/RemoveBorderEffect.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/RemoveBorder/RemoveBorderEffect.ios.cs index 8499ae09e..00d257d2b 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/RemoveBorder/RemoveBorderEffect.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/RemoveBorder/RemoveBorderEffect.ios.cs @@ -12,7 +12,7 @@ public class RemoveBorderEffect : PlatformEffect { UITextBorderStyle? oldBorderStyle; - UITextField TextField => Control as UITextField; + UITextField TextField => (UITextField)Control; protected override void OnAttached() { diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/RemoveBorder/RemoveBorderEffect.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/RemoveBorder/RemoveBorderEffect.shared.cs index 31700c0d6..656ca70a1 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/RemoveBorder/RemoveBorderEffect.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/RemoveBorder/RemoveBorderEffect.shared.cs @@ -16,6 +16,9 @@ public RemoveBorderEffect() #elif __ANDROID__ if (DateTime.Now.Ticks < 0) _ = new Xamarin.CommunityToolkit.Android.Effects.RemoveBorderEffect(); +#elif UWP + if (System.DateTime.Now.Ticks < 0) + _ = new Xamarin.CommunityToolkit.UWP.Effects.RemoveBorderEffect(); #endif #endregion } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/RemoveBorder/RemoveBorderEffect.uwp.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/RemoveBorder/RemoveBorderEffect.uwp.cs new file mode 100644 index 000000000..a04f0b71b --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/RemoveBorder/RemoveBorderEffect.uwp.cs @@ -0,0 +1,32 @@ +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Xamarin.CommunityToolkit.Effects; +using Xamarin.Forms.Platform.UWP; +using Effects = Xamarin.CommunityToolkit.UWP.Effects; + +[assembly: Xamarin.Forms.ExportEffect(typeof(Effects.RemoveBorderEffect), nameof(RemoveBorderEffect))] + +namespace Xamarin.CommunityToolkit.UWP.Effects +{ + public class RemoveBorderEffect : PlatformEffect + { + Thickness oldBorderThickness; + + protected override void OnAttached() + { + if (Control is Control uwpControl) + { + oldBorderThickness = uwpControl.BorderThickness; + uwpControl.BorderThickness = new Thickness(0.0); + } + } + + protected override void OnDetached() + { + if (Control is Control uwpControl) + { + uwpControl.BorderThickness = oldBorderThickness; + } + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SafeArea/SafeAreaEffectRouter.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SafeArea/SafeAreaEffectRouter.ios.cs index a32e9dd57..3c08bc081 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SafeArea/SafeAreaEffectRouter.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SafeArea/SafeAreaEffectRouter.ios.cs @@ -13,8 +13,7 @@ public class SafeAreaEffectRouter : PlatformEffect { Thickness initialMargin; - new View Element - => base.Element as View; + new View Element => (View)base.Element; bool IsEligibleToConsumeEffect => Element != null @@ -43,9 +42,6 @@ protected override void OnDetached() Element.Margin = initialMargin; } - double CalculateInsets(double insetsComponent, bool shouldUseInsetsComponent) - => shouldUseInsetsComponent - ? insetsComponent - : 0; + double CalculateInsets(double insetsComponent, bool shouldUseInsetsComponent) => shouldUseInsetsComponent ? insetsComponent : 0; } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SelectAllText/SelectAllTextEffect.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SelectAllText/SelectAllTextEffect.android.cs index e23daa03a..332334d31 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SelectAllText/SelectAllTextEffect.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SelectAllText/SelectAllTextEffect.android.cs @@ -10,7 +10,7 @@ namespace Xamarin.CommunityToolkit.Android.Effects { public class SelectAllTextEffect : PlatformEffect { - EditText EditText => Control as EditText; + EditText EditText => (EditText)Control; protected override void OnAttached() => EditText?.SetSelectAllOnFocus(true); @@ -18,4 +18,4 @@ protected override void OnAttached() protected override void OnDetached() => EditText?.SetSelectAllOnFocus(false); } -} \ No newline at end of file +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SelectAllText/SelectAllTextEffect.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SelectAllText/SelectAllTextEffect.ios.cs index e78a6b9d1..4f8cf2675 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SelectAllText/SelectAllTextEffect.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SelectAllText/SelectAllTextEffect.ios.cs @@ -16,16 +16,14 @@ public class SelectAllTextEffect : PlatformEffect protected override void OnDetached() => ApplyEffect(false); - void ApplyEffect(bool apply) - => ApplyToControl(Control, apply); + void ApplyEffect(bool apply) => ApplyToControl(Control, apply); - bool ApplyToControl(T controlType, bool apply) => - controlType switch - { - UITextField textField => ApplyToUITextField(textField, apply), - UITextView _ => ApplyToUITextView(apply), - _ => throw new NotSupportedException($"Control of type: {controlType.GetType().Name} is not supported by this effect.") - }; + bool ApplyToControl(T controlType, bool apply) => controlType switch + { + UITextField textField => ApplyToUITextField(textField, apply), + UITextView => ApplyToUITextView(apply), + _ => throw new NotSupportedException($"Control of type: {controlType?.GetType()?.Name} is not supported by this effect.") + }; #region - UITextField @@ -42,11 +40,9 @@ bool ApplyToUITextField(UITextField textField, bool apply) return true; } - void OnEditingDidBegin(object sender, EventArgs e) + void OnEditingDidBegin(object? sender, EventArgs e) { - var textfield = sender as UITextField; - - if (textfield == null) + if (sender is not UITextField textfield) return; textfield.PerformSelector(new Selector("selectAll"), null, 0.0f); @@ -58,8 +54,7 @@ void OnEditingDidBegin(object sender, EventArgs e) bool ApplyToUITextView(bool apply) { - var formsControl = Element as Editor; - if (formsControl == null) + if (Element is not Editor formsControl) return false; if (apply) @@ -70,14 +65,12 @@ bool ApplyToUITextView(bool apply) return true; } - void OnTextViewFocussed(object sender, FocusEventArgs e) + void OnTextViewFocussed(object? sender, FocusEventArgs e) { - var formsControl = Element as Editor; - if (formsControl == null) + if (Element is not Editor formsControl) return; - var textView = Control as UITextView; - if (textView == null) + if (Control is not UITextView textView) return; if (formsControl.IsFocused) diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SelectAllText/SelectAllTextEffect.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SelectAllText/SelectAllTextEffect.shared.cs index 92b64eadb..2ef56f69a 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SelectAllText/SelectAllTextEffect.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/SelectAllText/SelectAllTextEffect.shared.cs @@ -8,7 +8,7 @@ public class SelectAllTextEffect : RoutingEffect public SelectAllTextEffect() : base(EffectIds.SelectAllText) { - #region Required work-around to prevent linker from removing the platform-specific implementation + #region Required work-around to prevent linker from removing the platform-specific implementation #if __IOS__ if (DateTime.Now.Ticks < 0) _ = new Xamarin.CommunityToolkit.iOS.Effects.SelectAllTextEffect(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/PlatformShadowEffect.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/PlatformShadowEffect.android.cs new file mode 100644 index 000000000..385572c2e --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/PlatformShadowEffect.android.cs @@ -0,0 +1,90 @@ +using Xamarin.Forms.Platform.Android; +using Xamarin.Forms; +using Android.Views; +using AView = Android.Views.View; +using Android.OS; +using System.ComponentModel; +using Xamarin.CommunityToolkit.Effects; +using Xamarin.CommunityToolkit.Android.Effects; +using Android.Widget; + +[assembly: ExportEffect(typeof(PlatformShadowEffect), nameof(ShadowEffect))] + +namespace Xamarin.CommunityToolkit.Android.Effects +{ + public class PlatformShadowEffect : PlatformEffect + { + const float defaultRadius = 10f; + + const float defaultOpacity = 1f; + + AView View => Control ?? Container; + + protected override void OnAttached() + => Update(); + + protected override void OnDetached() + { + if (View == null) + return; + + View.Elevation = 0; + } + + protected override void OnElementPropertyChanged(PropertyChangedEventArgs args) + { + base.OnElementPropertyChanged(args); + + if (View == null) + return; + + switch (args.PropertyName) + { + case nameof(ShadowEffect.ColorPropertyName): + case nameof(ShadowEffect.OpacityPropertyName): + case nameof(ShadowEffect.RadiusPropertyName): + case nameof(ShadowEffect.OffsetXPropertyName): + case nameof(ShadowEffect.OffsetYPropertyName): + View.Invalidate(); + Update(); + break; + } + } + + void Update() + { + if (View == null || Build.VERSION.SdkInt < BuildVersionCodes.Lollipop) + return; + + var radius = (float)ShadowEffect.GetRadius(Element); + if (radius < 0) + radius = defaultRadius; + + var opacity = ShadowEffect.GetOpacity(Element); + if (opacity < 0) + opacity = defaultOpacity; + + var androidColor = ShadowEffect.GetColor(Element).MultiplyAlpha(opacity).ToAndroid(); + + if (View is TextView textView) + { + var offsetX = (float)ShadowEffect.GetOffsetX(Element); + var offsetY = (float)ShadowEffect.GetOffsetY(Element); + textView.SetShadowLayer(radius, offsetX, offsetY, androidColor); + return; + } + + View.OutlineProvider = (Element as VisualElement)?.BackgroundColor.A > 0 + ? ViewOutlineProvider.PaddedBounds + : ViewOutlineProvider.Bounds; + + View.Elevation = View.Context.ToPixels(radius); + + if (Build.VERSION.SdkInt < BuildVersionCodes.P) + return; + + View.SetOutlineAmbientShadowColor(androidColor); + View.SetOutlineSpotShadowColor(androidColor); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/PlatformShadowEffect.ios.macos.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/PlatformShadowEffect.ios.macos.cs new file mode 100644 index 000000000..bd639e4df --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/PlatformShadowEffect.ios.macos.cs @@ -0,0 +1,107 @@ +using System; +using System.ComponentModel; +using CoreGraphics; +using Xamarin.CommunityToolkit.Effects; +using Xamarin.Forms; + +#if __IOS__ +using NativeView = UIKit.UIView; +using Xamarin.Forms.Platform.iOS; +using Xamarin.CommunityToolkit.iOS.Effects; +#elif __MACOS__ +using NativeView = AppKit.NSView; +using Xamarin.Forms.Platform.MacOS; +using Xamarin.CommunityToolkit.macOS.Effects; +#endif + +[assembly: ExportEffect(typeof(PlatformShadowEffect), nameof(ShadowEffect))] + +#if __IOS__ +namespace Xamarin.CommunityToolkit.iOS.Effects +#elif __MACOS__ +namespace Xamarin.CommunityToolkit.macOS.Effects +#endif +{ + public class PlatformShadowEffect : PlatformEffect + { + const float defaultRadius = 10f; + + const float defaultOpacity = .5f; + + NativeView? View => Control ?? Container; + + protected override void OnAttached() + { + if (View == null) + return; + + UpdateColor(View); + UpdateOpacity(View); + UpdateRadius(View); + UpdateOffset(View); + } + + protected override void OnDetached() + { + if (View?.Layer == null) + return; + + View.Layer.ShadowOpacity = 0; + } + + protected override void OnElementPropertyChanged(PropertyChangedEventArgs args) + { + base.OnElementPropertyChanged(args); + + if (View == null) + return; + + switch (args.PropertyName) + { + case nameof(ShadowEffect.ColorPropertyName): + UpdateColor(View); + break; + case nameof(ShadowEffect.OpacityPropertyName): + UpdateOpacity(View); + break; + case nameof(ShadowEffect.RadiusPropertyName): + UpdateRadius(View); + break; + case nameof(ShadowEffect.OffsetXPropertyName): + case nameof(ShadowEffect.OffsetYPropertyName): + UpdateOffset(View); + break; + } + } + + void UpdateColor(in NativeView view) + { + if (view.Layer != null) + view.Layer.ShadowColor = ShadowEffect.GetColor(Element).ToCGColor(); + } + + void UpdateOpacity(in NativeView view) + { + if (view.Layer != null) + { + var opacity = (float)ShadowEffect.GetOpacity(Element); + view.Layer.ShadowOpacity = opacity < 0 ? defaultOpacity : opacity; + } + } + + void UpdateRadius(in NativeView view) + { + if (view.Layer != null) + { + var radius = (nfloat)ShadowEffect.GetRadius(Element); + view.Layer.ShadowRadius = radius < 0 ? defaultRadius : radius; + } + } + + void UpdateOffset(in NativeView view) + { + if (view.Layer != null) + view.Layer.ShadowOffset = new CGSize((double)ShadowEffect.GetOffsetX(Element), (double)ShadowEffect.GetOffsetY(Element)); + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/ShadowEffect.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/ShadowEffect.shared.cs new file mode 100644 index 000000000..0d5761122 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Shadow/ShadowEffect.shared.cs @@ -0,0 +1,117 @@ +using System.Linq; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.Effects +{ + public class ShadowEffect : RoutingEffect + { + internal const string ColorPropertyName = "Color"; + + internal const string OpacityPropertyName = "Opacity"; + + internal const string RadiusPropertyName = "Radius"; + + internal const string OffsetXPropertyName = "OffsetX"; + + internal const string OffsetYPropertyName = "OffsetY"; + + public static readonly BindableProperty ColorProperty = BindableProperty.CreateAttached( + ColorPropertyName, + typeof(Color), + typeof(ShadowEffect), + Color.Default, + propertyChanged: TryGenerateEffect); + + public static readonly BindableProperty OpacityProperty = BindableProperty.CreateAttached( + OpacityPropertyName, + typeof(double), + typeof(ShadowEffect), + -1.0, + propertyChanged: TryGenerateEffect); + + public static readonly BindableProperty RadiusProperty = BindableProperty.CreateAttached( + RadiusPropertyName, + typeof(double), + typeof(ShadowEffect), + -1.0, + propertyChanged: TryGenerateEffect); + + public static readonly BindableProperty OffsetXProperty = BindableProperty.CreateAttached( + OffsetXPropertyName, + typeof(double), + typeof(ShadowEffect), + .0, + propertyChanged: TryGenerateEffect); + + public static readonly BindableProperty OffsetYProperty = BindableProperty.CreateAttached( + OffsetYPropertyName, + typeof(double), + typeof(ShadowEffect), + .0, + propertyChanged: TryGenerateEffect); + + public ShadowEffect() + : base(EffectIds.ShadowEffect) + { +#if __ANDROID__ + if (System.DateTime.Now.Ticks < 0) + _ = new Xamarin.CommunityToolkit.Android.Effects.PlatformShadowEffect(); +#elif __IOS__ + if (System.DateTime.Now.Ticks < 0) + _ = new Xamarin.CommunityToolkit.iOS.Effects.PlatformShadowEffect(); +#elif __MACOS__ + if (System.DateTime.Now.Ticks < 0) + _ = new Xamarin.CommunityToolkit.macOS.Effects.PlatformShadowEffect(); +#endif + } + + public static Color GetColor(BindableObject bindable) + => (Color)bindable.GetValue(ColorProperty); + + public static void SetColor(BindableObject bindable, Color value) + => bindable.SetValue(ColorProperty, value); + + public static double GetOpacity(BindableObject bindable) + => (double)bindable.GetValue(OpacityProperty); + + public static void SetOpacity(BindableObject bindable, double value) + => bindable.SetValue(OpacityProperty, value); + + public static double GetRadius(BindableObject bindable) + => (double)bindable.GetValue(RadiusProperty); + + public static void SetRadius(BindableObject bindable, double value) + => bindable.SetValue(RadiusProperty, value); + + public static double GetOffsetX(BindableObject bindable) + => (double)bindable.GetValue(OffsetXProperty); + + public static void SetOffsetX(BindableObject bindable, double value) + => bindable.SetValue(OffsetXProperty, value); + + public static double GetOffsetY(BindableObject bindable) + => (double)bindable.GetValue(OffsetYProperty); + + public static void SetOffsetY(BindableObject bindable, double value) + => bindable.SetValue(OffsetYProperty, value); + + static void TryGenerateEffect(BindableObject bindable, object oldValue, object newValue) + { + if (!(bindable is VisualElement view)) + return; + + var shadowEffects = view.Effects.OfType(); + + if (GetColor(view) == Color.Default) + { + foreach (var effect in shadowEffects.ToArray()) + view.Effects.Remove(effect); + + return; + } + + if (!shadowEffects.Any()) + view.Effects.Add(new ShadowEffect()); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/EventArgs/LongPressCompletedEventArgs.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/EventArgs/LongPressCompletedEventArgs.shared.cs new file mode 100644 index 000000000..d5f08e0f2 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/EventArgs/LongPressCompletedEventArgs.shared.cs @@ -0,0 +1,12 @@ +using System; + +namespace Xamarin.CommunityToolkit.Effects +{ + public class LongPressCompletedEventArgs : EventArgs + { + internal LongPressCompletedEventArgs(object? parameter) + => Parameter = parameter; + + public object? Parameter { get; } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/EventArgs/TouchCompletedEventArgs.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/EventArgs/TouchCompletedEventArgs.shared.cs index d339e35db..502a10853 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/EventArgs/TouchCompletedEventArgs.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/EventArgs/TouchCompletedEventArgs.shared.cs @@ -4,9 +4,9 @@ namespace Xamarin.CommunityToolkit.Effects { public class TouchCompletedEventArgs : EventArgs { - internal TouchCompletedEventArgs(object parameter) + internal TouchCompletedEventArgs(object? parameter) => Parameter = parameter; - public object Parameter { get; } + public object? Parameter { get; } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/GestureManager.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/GestureManager.shared.cs index 939265d3d..9a7712cf3 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/GestureManager.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/GestureManager.shared.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Xamarin.CommunityToolkit.Extensions; +using Xamarin.CommunityToolkit.Helpers; using Xamarin.Forms; using static System.Math; @@ -15,11 +16,11 @@ sealed class GestureManager Color defaultBackgroundColor; - CancellationTokenSource longPressTokenSource; + CancellationTokenSource? longPressTokenSource; - CancellationTokenSource animationTokenSource; + CancellationTokenSource? animationTokenSource; - Func animationTaskFactory; + Func? animationTaskFactory; double? durationMultiplier; @@ -59,11 +60,13 @@ internal void HandleTouch(TouchEffect sender, TouchStatus status) : animationProgress; UpdateStatusAndState(sender, status, state); + if (status == TouchStatus.Canceled) { sender.ForceUpdateState(false); return; } + OnTapped(sender); sender.IsToggled = !isToggled; return; @@ -92,7 +95,7 @@ internal void HandleUserInteraction(TouchEffect sender, TouchInteractionStatus i internal void HandleHover(TouchEffect sender, HoverStatus status) { - if (!sender.Element.IsEnabled) + if (!sender.Element?.IsEnabled ?? true) return; var hoverState = status == HoverStatus.Entered @@ -123,7 +126,8 @@ internal async Task ChangeStateAsync(TouchEffect sender, bool animated) var isToggled = sender.IsToggled; - UpdateVisualState(sender.Element, state, hoverState); + if (sender.Element != null) + UpdateVisualState(sender.Element, state, hoverState); if (!animated) { @@ -133,9 +137,11 @@ internal async Task ChangeStateAsync(TouchEffect sender, bool animated) ? TouchState.Pressed : TouchState.Normal; } + var durationMultiplier = this.durationMultiplier; this.durationMultiplier = null; - await GetAnimationTask(sender, state, hoverState, durationMultiplier.GetValueOrDefault()).ConfigureAwait(false); + + await RunAnimationTask(sender, state, hoverState, animationTokenSource.Token, durationMultiplier.GetValueOrDefault()).ConfigureAwait(false); return; } @@ -143,7 +149,7 @@ internal async Task ChangeStateAsync(TouchEffect sender, bool animated) if (pulseCount == 0 || (state == TouchState.Normal && !isToggled.HasValue)) { - await GetAnimationTask(sender, state, hoverState).ConfigureAwait(false); + await RunAnimationTask(sender, state, hoverState, animationTokenSource.Token).ConfigureAwait(false); return; } do @@ -152,7 +158,7 @@ internal async Task ChangeStateAsync(TouchEffect sender, bool animated) ? TouchState.Normal : TouchState.Pressed; - await GetAnimationTask(sender, rippleState, hoverState); + await RunAnimationTask(sender, rippleState, hoverState, animationTokenSource.Token); if (token.IsCancellationRequested) return; @@ -160,7 +166,7 @@ internal async Task ChangeStateAsync(TouchEffect sender, bool animated) ? TouchState.Pressed : TouchState.Normal; - await GetAnimationTask(sender, rippleState, hoverState); + await RunAnimationTask(sender, rippleState, hoverState, animationTokenSource.Token); if (token.IsCancellationRequested) return; } @@ -183,13 +189,16 @@ internal void HandleLongPress(TouchEffect sender) longPressTokenSource = new CancellationTokenSource(); Task.Delay(sender.LongPressDuration, longPressTokenSource.Token).ContinueWith(t => { + if (t.IsFaulted && t.Exception != null) + throw t.Exception; + if (t.IsCanceled) return; var longPressAction = new Action(() => { sender.HandleUserInteraction(TouchInteractionStatus.Completed); - sender.LongPressCommand?.Execute(sender.LongPressCommandParameter ?? sender.CommandParameter); + sender.RaiseLongPressCompleted(); }); if (Device.IsInvokeRequired) @@ -199,7 +208,7 @@ internal void HandleLongPress(TouchEffect sender) }); } - internal void SetCustomAnimationTask(Func animationTaskFactory) + internal void SetCustomAnimationTask(Func? animationTaskFactory) => this.animationTaskFactory = animationTaskFactory; internal void Reset() @@ -219,16 +228,16 @@ internal void OnTapped(TouchEffect sender) if (sender.Element is IButtonController button) button.SendClicked(); - sender.Command?.Execute(sender.CommandParameter); sender.RaiseCompleted(); } void HandleCollectionViewSelection(TouchEffect sender) { - if (!sender.Element.TryFindParentElementWithParentOfType(out var element, out CollectionView collectionView)) + if (!sender.Element.TryFindParentElementWithParentOfType(out var result, out CollectionView? parent)) return; - var item = element.BindingContext ?? element; + var collectionView = parent ?? throw new NullReferenceException(); + var item = result?.BindingContext ?? result ?? throw new NullReferenceException(); switch (collectionView.SelectionMode) { @@ -267,6 +276,7 @@ void UpdateStatusAndState(TouchEffect sender, TouchStatus status, TouchState sta sender.State = state; sender.RaiseStateChanged(); } + sender.Status = status; sender.RaiseStatusChanged(); } @@ -274,10 +284,10 @@ void UpdateStatusAndState(TouchEffect sender, TouchStatus status, TouchState sta void UpdateVisualState(VisualElement visualElement, TouchState touchState, HoverState hoverState) { var state = touchState == TouchState.Pressed - ? nameof(TouchState.Pressed) + ? TouchEffect.PressedVisualState : hoverState == HoverState.Hovered - ? nameof(HoverState.Hovered) - : nameof(TouchState.Normal); + ? TouchEffect.HoveredVisualState + : TouchEffect.UnpressedVisualState; VisualStateManager.GoToState(visualElement, state); } @@ -297,22 +307,22 @@ async Task SetBackgroundImageAsync(TouchEffect sender, TouchState touchState, Ho var source = normalBackgroundImageSource; if (touchState == TouchState.Pressed) { - if (sender.Element.IsSet(TouchEffect.PressedBackgroundImageAspectProperty)) + if (sender.Element?.IsSet(TouchEffect.PressedBackgroundImageAspectProperty) ?? false) aspect = sender.PressedBackgroundImageAspect; source = pressedBackgroundImageSource; } else if (hoverState == HoverState.Hovered) { - if (sender.Element.IsSet(TouchEffect.HoveredBackgroundImageAspectProperty)) + if (sender.Element?.IsSet(TouchEffect.HoveredBackgroundImageAspectProperty) ?? false) aspect = sender.HoveredBackgroundImageAspect; - if (sender.Element.IsSet(TouchEffect.HoveredBackgroundImageSourceProperty)) + if (sender.Element?.IsSet(TouchEffect.HoveredBackgroundImageSourceProperty) ?? false) source = hoveredBackgroundImageSource; } else { - if (sender.Element.IsSet(TouchEffect.NormalBackgroundImageAspectProperty)) + if (sender.Element?.IsSet(TouchEffect.NormalBackgroundImageAspectProperty) ?? false) aspect = sender.NormalBackgroundImageAspect; } @@ -336,16 +346,19 @@ async Task SetBackgroundImageAsync(TouchEffect sender, TouchState touchState, Ho } } - Task SetBackgroundColor(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing) + Task SetBackgroundColor(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing) { var normalBackgroundColor = sender.NormalBackgroundColor; var pressedBackgroundColor = sender.PressedBackgroundColor; var hoveredBackgroundColor = sender.HoveredBackgroundColor; - if (normalBackgroundColor == Color.Default && - pressedBackgroundColor == Color.Default && - hoveredBackgroundColor == Color.Default) + if (sender.Element == null + || (normalBackgroundColor == Color.Default + && pressedBackgroundColor == Color.Default + && hoveredBackgroundColor == Color.Default)) + { return Task.FromResult(false); + } var element = sender.Element; if (defaultBackgroundColor == default) @@ -368,7 +381,7 @@ Task SetBackgroundColor(TouchEffect sender, TouchState touchState, HoverState ho return element.ColorTo(color, (uint)duration, easing); } - Task SetOpacity(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing) + Task SetOpacity(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing) { var normalOpacity = sender.NormalOpacity; var pressedOpacity = sender.PressedOpacity; @@ -383,11 +396,11 @@ Task SetOpacity(TouchEffect sender, TouchState touchState, HoverState hoverState if (touchState == TouchState.Pressed) opacity = pressedOpacity; - else if (hoverState == HoverState.Hovered && sender.Element.IsSet(TouchEffect.HoveredOpacityProperty)) + else if (hoverState == HoverState.Hovered && (sender.Element?.IsSet(TouchEffect.HoveredOpacityProperty) ?? false)) opacity = hoveredOpacity; var element = sender.Element; - if (duration <= 0) + if (duration <= 0 && element != null) { element.AbortAnimations(); element.Opacity = opacity; @@ -397,7 +410,7 @@ Task SetOpacity(TouchEffect sender, TouchState touchState, HoverState hoverState return element.FadeTo(opacity, (uint)Abs(duration), easing); } - Task SetScale(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing) + Task SetScale(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing) { var normalScale = sender.NormalScale; var pressedScale = sender.PressedScale; @@ -412,10 +425,13 @@ Task SetScale(TouchEffect sender, TouchState touchState, HoverState hoverState, if (touchState == TouchState.Pressed) scale = pressedScale; - else if (hoverState == HoverState.Hovered && sender.Element.IsSet(TouchEffect.HoveredScaleProperty)) + else if (hoverState == HoverState.Hovered && (sender.Element?.IsSet(TouchEffect.HoveredScaleProperty) ?? false)) scale = hoveredScale; var element = sender.Element; + if (element == null) + return Task.FromResult(false); + if (duration <= 0) { element.AbortAnimations(nameof(SetScale)); @@ -434,7 +450,7 @@ Task SetScale(TouchEffect sender, TouchState touchState, HoverState hoverState, return animationCompletionSource.Task; } - Task SetTranslation(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing) + Task SetTranslation(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing) { var normalTranslationX = sender.NormalTranslationX; var pressedTranslationX = sender.PressedTranslationX; @@ -444,13 +460,15 @@ Task SetTranslation(TouchEffect sender, TouchState touchState, HoverState hoverS var pressedTranslationY = sender.PressedTranslationY; var hoveredTranslationY = sender.HoveredTranslationY; - if (Abs(normalTranslationX) <= double.Epsilon && - Abs(pressedTranslationX) <= double.Epsilon && - Abs(hoveredTranslationX) <= double.Epsilon && - Abs(normalTranslationY) <= double.Epsilon && - Abs(pressedTranslationY) <= double.Epsilon && - Abs(hoveredTranslationY) <= double.Epsilon) + if (Abs(normalTranslationX) <= double.Epsilon + && Abs(pressedTranslationX) <= double.Epsilon + && Abs(hoveredTranslationX) <= double.Epsilon + && Abs(normalTranslationY) <= double.Epsilon + && Abs(pressedTranslationY) <= double.Epsilon + && Abs(hoveredTranslationY) <= double.Epsilon) + { return Task.FromResult(false); + } var translationX = normalTranslationX; var translationY = normalTranslationY; @@ -462,15 +480,15 @@ Task SetTranslation(TouchEffect sender, TouchState touchState, HoverState hoverS } else if (hoverState == HoverState.Hovered) { - if (sender.Element.IsSet(TouchEffect.HoveredTranslationXProperty)) + if (sender.Element?.IsSet(TouchEffect.HoveredTranslationXProperty) ?? false) translationX = hoveredTranslationX; - if (sender.Element.IsSet(TouchEffect.HoveredTranslationYProperty)) + if (sender.Element?.IsSet(TouchEffect.HoveredTranslationYProperty) ?? false) translationY = hoveredTranslationY; } var element = sender.Element; - if (duration <= 0) + if (duration <= 0 && element != null) { element.AbortAnimations(); element.TranslationX = translationX; @@ -478,39 +496,41 @@ Task SetTranslation(TouchEffect sender, TouchState touchState, HoverState hoverS return Task.FromResult(true); } - return element.TranslateTo(translationX, translationY, (uint)Abs(duration), easing); + return element?.TranslateTo(translationX, translationY, (uint)Abs(duration), easing) ?? Task.FromResult(false); } - Task SetRotation(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing) + Task SetRotation(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing) { var normalRotation = sender.NormalRotation; var pressedRotation = sender.PressedRotation; var hoveredRotation = sender.HoveredRotation; - if (Abs(normalRotation) <= double.Epsilon && - Abs(pressedRotation) <= double.Epsilon && - Abs(hoveredRotation) <= double.Epsilon) + if (Abs(normalRotation) <= double.Epsilon + && Abs(pressedRotation) <= double.Epsilon + && Abs(hoveredRotation) <= double.Epsilon) + { return Task.FromResult(false); + } var rotation = normalRotation; if (touchState == TouchState.Pressed) rotation = pressedRotation; - else if (hoverState == HoverState.Hovered && sender.Element.IsSet(TouchEffect.HoveredRotationProperty)) + else if (hoverState == HoverState.Hovered && (sender.Element?.IsSet(TouchEffect.HoveredRotationProperty) ?? false)) rotation = hoveredRotation; var element = sender.Element; - if (duration <= 0) + if (duration <= 0 && element != null) { element.AbortAnimations(); element.Rotation = rotation; return Task.FromResult(true); } - return element.RotateTo(rotation, (uint)Abs(duration), easing); + return element?.RotateTo(rotation, (uint)Abs(duration), easing) ?? Task.FromResult(false); } - Task SetRotationX(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing) + Task SetRotationX(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing) { var normalRotationX = sender.NormalRotationX; var pressedRotationX = sender.PressedRotationX; @@ -525,21 +545,21 @@ Task SetRotationX(TouchEffect sender, TouchState touchState, HoverState hoverSta if (touchState == TouchState.Pressed) rotationX = pressedRotationX; - else if (hoverState == HoverState.Hovered && sender.Element.IsSet(TouchEffect.HoveredRotationXProperty)) + else if (hoverState == HoverState.Hovered && (sender.Element?.IsSet(TouchEffect.HoveredRotationXProperty) ?? false)) rotationX = hoveredRotationX; var element = sender.Element; - if (duration <= 0) + if (duration <= 0 && element != null) { element.AbortAnimations(); element.RotationX = rotationX; return Task.FromResult(true); } - return element.RotateXTo(rotationX, (uint)Abs(duration), easing); + return element?.RotateXTo(rotationX, (uint)Abs(duration), easing) ?? Task.FromResult(false); } - Task SetRotationY(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing easing) + Task SetRotationY(TouchEffect sender, TouchState touchState, HoverState hoverState, int duration, Easing? easing) { var normalRotationY = sender.NormalRotationY; var pressedRotationY = sender.PressedRotationY; @@ -554,18 +574,18 @@ Task SetRotationY(TouchEffect sender, TouchState touchState, HoverState hoverSta if (touchState == TouchState.Pressed) rotationY = pressedRotationY; - else if (hoverState == HoverState.Hovered && sender.Element.IsSet(TouchEffect.HoveredRotationYProperty)) + else if (hoverState == HoverState.Hovered && (sender.Element?.IsSet(TouchEffect.HoveredRotationYProperty) ?? false)) rotationY = hoveredRotationY; var element = sender.Element; - if (duration <= 0) + if (duration <= 0 && element != null) { element.AbortAnimations(); element.RotationY = rotationY; return Task.FromResult(true); } - return element.RotateYTo(rotationY, (uint)Abs(duration), easing); + return element?.RotateYTo(rotationY, (uint)Abs(duration), easing) ?? Task.FromResult(false); } Color GetBackgroundColor(Color color) @@ -573,12 +593,11 @@ Color GetBackgroundColor(Color color) ? color : defaultBackgroundColor; - Task GetAnimationTask(TouchEffect sender, TouchState touchState, HoverState hoverState, double? durationMultiplier = null) + Task RunAnimationTask(TouchEffect sender, TouchState touchState, HoverState hoverState, CancellationToken token, double? durationMultiplier = null) { if (sender.Element == null) return Task.FromResult(false); - var token = animationTokenSource.Token; var duration = sender.AnimationDuration; var easing = sender.AnimationEasing; diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.android.cs index 11f830c58..9e21ce3fd 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.android.cs @@ -22,12 +22,12 @@ public class PlatformTouchEffect : PlatformEffect { static readonly Forms.Color defaultNativeAnimationColor = Forms.Color.FromRgba(128, 128, 128, 64); - AccessibilityManager accessibilityManager; - AccessibilityListener accessibilityListener; - TouchEffect effect; + AccessibilityManager? accessibilityManager; + AccessibilityListener? accessibilityListener; + TouchEffect? effect; bool isHoverSupported; - RippleDrawable ripple; - AView rippleView; + RippleDrawable? ripple; + AView? rippleView; float startX; float startY; Forms.Color rippleColor; @@ -35,14 +35,13 @@ public class PlatformTouchEffect : PlatformEffect AView View => Control ?? Container; - ViewGroup Group => Container ?? Control as ViewGroup; + ViewGroup? Group => Container ?? Control as ViewGroup; internal bool IsCanceled { get; set; } - bool IsAccessibilityMode => - accessibilityManager != null && - accessibilityManager.IsEnabled && - accessibilityManager.IsTouchExplorationEnabled; + bool IsAccessibilityMode => accessibilityManager != null + && accessibilityManager.IsEnabled + && accessibilityManager.IsTouchExplorationEnabled; protected override void OnAttached() { @@ -58,7 +57,7 @@ protected override void OnAttached() View.Touch += OnTouch; UpdateClickHandler(); - accessibilityManager = View.Context.GetSystemService(Context.AccessibilityService) as AccessibilityManager; + accessibilityManager = View.Context?.GetSystemService(Context.AccessibilityService) as AccessibilityManager; if (accessibilityManager != null) { accessibilityListener = new AccessibilityListener(this); @@ -81,7 +80,7 @@ protected override void OnAttached() return; } - rippleView = new FrameLayout(Group.Context) + rippleView = new FrameLayout(Group.Context ?? throw new NullReferenceException()) { LayoutParameters = new ViewGroup.LayoutParams(-1, -1), Clickable = false, @@ -101,7 +100,7 @@ protected override void OnDetached() try { - if (accessibilityManager != null) + if (accessibilityManager != null && accessibilityListener != null) { accessibilityManager.RemoveAccessibilityStateChangeListener(accessibilityListener); accessibilityManager.RemoveTouchExplorationStateChangeListener(accessibilityListener); @@ -155,14 +154,14 @@ protected override void OnElementPropertyChanged(PropertyChangedEventArgs args) void UpdateClickHandler() { View.Click -= OnClick; - if (IsAccessibilityMode || (effect.IsAvailable && effect.Element.IsEnabled)) + if (IsAccessibilityMode || ((effect?.IsAvailable ?? false) && (effect?.Element?.IsEnabled ?? false))) { View.Click += OnClick; return; } } - void OnTouch(object sender, AView.TouchEventArgs e) + void OnTouch(object? sender, AView.TouchEventArgs e) { e.Handled = false; @@ -172,7 +171,7 @@ void OnTouch(object sender, AView.TouchEventArgs e) if (IsAccessibilityMode) return; - switch (e.Event.ActionMasked) + switch (e.Event?.ActionMasked) { case MotionEventActions.Down: OnTouchDown(e); @@ -197,52 +196,59 @@ void OnTouch(object sender, AView.TouchEventArgs e) void OnTouchDown(AView.TouchEventArgs e) { + _ = e.Event ?? throw new NullReferenceException(); + IsCanceled = false; + startX = e.Event.GetX(); startY = e.Event.GetY(); + effect?.HandleUserInteraction(TouchInteractionStatus.Started); effect?.HandleTouch(TouchStatus.Started); + StartRipple(e.Event.GetX(), e.Event.GetY()); - if (effect.DisallowTouchThreshold > 0) + + if (effect?.DisallowTouchThreshold > 0) Group?.Parent?.RequestDisallowInterceptTouchEvent(true); } void OnTouchUp() - => HandleEnd(effect.Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled); + => HandleEnd(effect?.Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled); void OnTouchCancel() => HandleEnd(TouchStatus.Canceled); - void OnTouchMove(object sender, AView.TouchEventArgs e) + void OnTouchMove(object? sender, AView.TouchEventArgs e) { - if (IsCanceled) + if (IsCanceled || e.Event == null) return; - var diffX = Math.Abs(e.Event.GetX() - startX) / View.Context.Resources.DisplayMetrics.Density; - var diffY = Math.Abs(e.Event.GetY() - startY) / View.Context.Resources.DisplayMetrics.Density; + var diffX = Math.Abs(e.Event.GetX() - startX) / View.Context?.Resources?.DisplayMetrics?.Density ?? throw new NullReferenceException(); + var diffY = Math.Abs(e.Event.GetY() - startY) / View.Context?.Resources?.DisplayMetrics?.Density ?? throw new NullReferenceException(); var maxDiff = Math.Max(diffX, diffY); - var disallowTouchThreshold = effect.DisallowTouchThreshold; + + var disallowTouchThreshold = effect?.DisallowTouchThreshold; if (disallowTouchThreshold > 0 && maxDiff > disallowTouchThreshold) { HandleEnd(TouchStatus.Canceled); return; } - var view = sender as AView; - if (view == null) + if (sender is not AView view) return; var screenPointerCoords = new Point(view.Left + e.Event.GetX(), view.Top + e.Event.GetY()); var viewRect = new Rectangle(view.Left, view.Top, view.Right - view.Left, view.Bottom - view.Top); var status = viewRect.Contains(screenPointerCoords) ? TouchStatus.Started : TouchStatus.Canceled; - if (isHoverSupported && ((status == TouchStatus.Canceled && effect.HoverStatus == HoverStatus.Entered) - || (status == TouchStatus.Started && effect.HoverStatus == HoverStatus.Exited))) + if (isHoverSupported && ((status == TouchStatus.Canceled && effect?.HoverStatus == HoverStatus.Entered) + || (status == TouchStatus.Started && effect?.HoverStatus == HoverStatus.Exited))) effect?.HandleHover(status == TouchStatus.Started ? HoverStatus.Entered : HoverStatus.Exited); - if (effect.Status != status) + if (effect?.Status != status) { effect?.HandleTouch(status); + if (status == TouchStatus.Started) StartRipple(e.Event.GetX(), e.Event.GetY()); if (status == TouchStatus.Canceled) @@ -262,7 +268,7 @@ void OnHoverExit() effect?.HandleHover(HoverStatus.Exited); } - void OnClick(object sender, EventArgs args) + void OnClick(object? sender, EventArgs args) { if (effect?.IsDisabled ?? true) return; @@ -280,11 +286,13 @@ void HandleEnd(TouchStatus status) return; IsCanceled = true; - if (effect.DisallowTouchThreshold > 0) + if (effect?.DisallowTouchThreshold > 0) Group?.Parent?.RequestDisallowInterceptTouchEvent(false); effect?.HandleTouch(status); + effect?.HandleUserInteraction(TouchInteractionStatus.Completed); + EndRipple(); } @@ -298,7 +306,7 @@ void StartRipple(float x, float y) UpdateRipple(); rippleView.Enabled = true; rippleView.BringToFront(); - ripple.SetHotspot(x, y); + ripple?.SetHotspot(x, y); rippleView.Pressed = true; } } @@ -323,8 +331,8 @@ void CreateRipple() var isEmptyDrawable = Element is Layout || drawable == null; - if (drawable is RippleDrawable) - ripple = (RippleDrawable)drawable.GetConstantState().NewDrawable(); + if (drawable is RippleDrawable rippleDrawable && rippleDrawable.GetConstantState() is Drawable.ConstantState constantState) + ripple = (RippleDrawable)constantState.NewDrawable(); else ripple = new RippleDrawable(GetColorStateList(), isEmptyDrawable ? null : drawable, isEmptyDrawable ? new ColorDrawable(Color.White) : null); @@ -341,13 +349,15 @@ void UpdateRipple() rippleColor = effect.NativeAnimationColor; rippleRadius = effect.NativeAnimationRadius; - ripple.SetColor(GetColorStateList()); - if (Build.VERSION.SdkInt >= BuildVersionCodes.M) - ripple.Radius = (int)(View.Context.Resources.DisplayMetrics.Density * effect.NativeAnimationRadius); + ripple?.SetColor(GetColorStateList()); + if (Build.VERSION.SdkInt >= BuildVersionCodes.M && ripple != null) + ripple.Radius = (int)(View.Context?.Resources?.DisplayMetrics?.Density * effect?.NativeAnimationRadius ?? throw new NullReferenceException()); } ColorStateList GetColorStateList() { + _ = effect?.NativeAnimationColor ?? throw new NullReferenceException(); + var nativeAnimationColor = effect.NativeAnimationColor; if (nativeAnimationColor == Forms.Color.Default) nativeAnimationColor = defaultNativeAnimationColor; @@ -357,9 +367,9 @@ ColorStateList GetColorStateList() new[] { (int)nativeAnimationColor.ToAndroid() }); } - void OnLayoutChange(object sender, AView.LayoutChangeEventArgs e) + void OnLayoutChange(object? sender, AView.LayoutChangeEventArgs e) { - if (!(sender is AView view) || (Group as IVisualElementRenderer)?.Element == null || rippleView == null) + if (sender is not AView view || (Group as IVisualElementRenderer)?.Element == null || rippleView == null) return; rippleView.Right = view.Width; @@ -370,7 +380,7 @@ sealed class AccessibilityListener : Java.Lang.Object, AccessibilityManager.IAccessibilityStateChangeListener, AccessibilityManager.ITouchExplorationStateChangeListener { - PlatformTouchEffect platformTouchEffect; + PlatformTouchEffect? platformTouchEffect; internal AccessibilityListener(PlatformTouchEffect platformTouchEffect) => this.platformTouchEffect = platformTouchEffect; diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.ios.cs index 3d7b150dd..deb45f1c7 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.ios.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using CoreGraphics; using Foundation; using UIKit; @@ -14,11 +15,11 @@ namespace Xamarin.CommunityToolkit.iOS.Effects { public class PlatformTouchEffect : PlatformEffect { - UIGestureRecognizer touchGesture; + UIGestureRecognizer? touchGesture; - UIGestureRecognizer hoverGesture; + UIGestureRecognizer? hoverGesture; - TouchEffect effect; + TouchEffect? effect; UIView View => Container ?? Control; @@ -80,10 +81,10 @@ protected override void OnDetached() void OnHover() { - if (effect?.IsDisabled ?? true) + if (effect == null || effect.IsDisabled) return; - switch (hoverGesture.State) + switch (hoverGesture?.State) { case UIGestureRecognizerState.Began: case UIGestureRecognizerState.Changed: @@ -95,13 +96,18 @@ void OnHover() } } - void PreventButtonHighlight(object sender, EventArgs args) - => ((UIButton)sender).Highlighted = false; + void PreventButtonHighlight(object? sender, EventArgs args) + { + if (sender is not UIButton button) + throw new ArgumentException($"{nameof(sender)} must be Type {nameof(UIButton)}", nameof(sender)); + + button.Highlighted = false; + } } sealed class TouchUITapGestureRecognizer : UIGestureRecognizer { - TouchEffect effect; + TouchEffect? effect; float? defaultRadius; float? defaultShadowRadius; float? defaultShadowOpacity; @@ -118,7 +124,7 @@ public TouchUITapGestureRecognizer(TouchEffect effect) public bool IsButton { get; set; } - UIView Renderer => effect?.Element.GetRenderer() as UIView; + UIView? Renderer => (UIView?)effect?.Element.GetRenderer(); public override void TouchesBegan(NSSet touches, UIEvent evt) { @@ -127,7 +133,9 @@ public override void TouchesBegan(NSSet touches, UIEvent evt) IsCanceled = false; startPoint = GetTouchPoint(touches); - HandleTouch(TouchStatus.Started, TouchInteractionStatus.Started); + + HandleTouch(TouchStatus.Started, TouchInteractionStatus.Started).SafeFireAndForget(); + base.TouchesBegan(touches, evt); } @@ -136,8 +144,10 @@ public override void TouchesEnded(NSSet touches, UIEvent evt) if (effect?.IsDisabled ?? true) return; - HandleTouch(effect?.Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled, TouchInteractionStatus.Completed); + HandleTouch(effect?.Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget(); + IsCanceled = true; + base.TouchesEnded(touches, evt); } @@ -146,8 +156,10 @@ public override void TouchesCancelled(NSSet touches, UIEvent evt) if (effect?.IsDisabled ?? true) return; - HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed); + HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget(); + IsCanceled = true; + base.TouchesCancelled(touches, evt); } @@ -165,19 +177,19 @@ public override void TouchesMoved(NSSet touches, UIEvent evt) var maxDiff = Math.Max(diffX, diffY); if (maxDiff > disallowTouchThreshold) { - HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed); + HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).SafeFireAndForget(); IsCanceled = true; base.TouchesMoved(touches, evt); return; } } - var status = point != null && Renderer.Bounds.Contains(point.Value) + var status = point != null && Renderer?.Bounds.Contains(point.Value) is true ? TouchStatus.Started : TouchStatus.Canceled; if (effect?.Status != status) - HandleTouch(status); + HandleTouch(status).SafeFireAndForget(); base.TouchesMoved(touches, evt); } @@ -187,15 +199,16 @@ protected override void Dispose(bool disposing) if (disposing) { effect = null; - Delegate = null; + Delegate.Dispose(); } + base.Dispose(disposing); } CGPoint? GetTouchPoint(NSSet touches) => Renderer != null ? (touches?.AnyObject as UITouch)?.LocationInView(Renderer) : null; - public void HandleTouch(TouchStatus status, TouchInteractionStatus? interactionStatus = null) + public async Task HandleTouch(TouchStatus status, TouchInteractionStatus? interactionStatus = null) { if (IsCanceled || effect == null) return; @@ -209,7 +222,7 @@ public void HandleTouch(TouchStatus status, TouchInteractionStatus? interactionS interactionStatus = null; } - effect.HandleTouch(status); + effect?.HandleTouch(status); if (interactionStatus.HasValue) effect?.HandleUserInteraction(interactionStatus.Value); @@ -217,7 +230,7 @@ public void HandleTouch(TouchStatus status, TouchInteractionStatus? interactionS return; var control = effect.Element; - if (!(control?.GetRenderer() is UIView renderer)) + if (control?.GetRenderer() is not UIView renderer) return; var color = effect.NativeAnimationColor; @@ -228,7 +241,7 @@ public void HandleTouch(TouchStatus status, TouchInteractionStatus? interactionS defaultShadowRadius = (float?)(defaultShadowRadius ?? renderer.Layer.ShadowRadius); defaultShadowOpacity ??= renderer.Layer.ShadowOpacity; - UIView.AnimateAsync(.2, () => + await UIView.AnimateAsync(.2, () => { if (color == Color.Default) renderer.Layer.Opacity = isStarted ? 0.5f : (float)control.Opacity; @@ -253,8 +266,13 @@ public override bool ShouldRecognizeSimultaneously(UIGestureRecognizer gestureRe if (gestureRecognizer is TouchUITapGestureRecognizer touchGesture && otherGestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer.State == UIGestureRecognizerState.Began) { - touchGesture.HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed); - touchGesture.IsCanceled = true; + touchGesture.HandleTouch(TouchStatus.Canceled, TouchInteractionStatus.Completed).ContinueWith(task => + { + if (task.IsFaulted && task.Exception != null) + throw task.Exception; + + touchGesture.IsCanceled = true; + }); } return true; diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.macos.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.macos.cs index 065abfb0f..19d4b2656 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.macos.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.macos.cs @@ -1,4 +1,5 @@ -using AppKit; +using System; +using AppKit; using Xamarin.CommunityToolkit.Effects; using Xamarin.CommunityToolkit.macOS.Effects; using Xamarin.Forms; @@ -10,11 +11,11 @@ namespace Xamarin.CommunityToolkit.macOS.Effects { public class PlatformTouchEffect : PlatformEffect { - NSGestureRecognizer gesture; + NSGestureRecognizer? gesture; - TouchEffect effect; + TouchEffect? effect; - MouseTrackingView mouseTrackingView; + MouseTrackingView? mouseTrackingView; protected override void OnAttached() { @@ -39,21 +40,25 @@ protected override void OnDetached() mouseTrackingView?.RemoveFromSuperview(); mouseTrackingView?.Dispose(); + mouseTrackingView = null; effect.Element = null; effect = null; + if (gesture != null) + { Container?.RemoveGestureRecognizer(gesture); + gesture.Dispose(); + } - gesture?.Dispose(); gesture = null; } } sealed class MouseTrackingView : NSView { - NSTrackingArea trackingArea; - TouchEffect effect; + NSTrackingArea? trackingArea; + TouchEffect? effect; public MouseTrackingView(TouchEffect effect) { @@ -74,7 +79,7 @@ public override void UpdateTrackingAreas() public override void MouseEntered(NSEvent theEvent) { - if (effect?.IsDisabled ?? true) + if (effect?.Element == null || effect.IsDisabled) return; effect?.HandleHover(HoverStatus.Entered); @@ -82,7 +87,7 @@ public override void MouseEntered(NSEvent theEvent) public override void MouseExited(NSEvent theEvent) { - if (effect?.IsDisabled ?? true) + if (effect?.Element == null || effect.IsDisabled) return; effect?.HandleHover(HoverStatus.Exited); @@ -99,14 +104,15 @@ protected override void Dispose(bool disposing) } effect = null; } + base.Dispose(disposing); } } sealed class TouchNSClickGestureRecognizer : NSGestureRecognizer { - TouchEffect effect; - NSView container; + TouchEffect? effect; + NSView? container; public TouchNSClickGestureRecognizer(TouchEffect effect, NSView container) { @@ -118,30 +124,33 @@ Rectangle ViewRect { get { - var frame = container.Frame; - var parent = container.Superview; + var frame = container?.Frame ?? throw new NullReferenceException(); + var parent = container?.Superview; + while (parent != null) { frame = new CoreGraphics.CGRect(frame.X + parent.Frame.X, frame.Y + parent.Frame.Y, frame.Width, frame.Height); parent = parent.Superview; } + return frame.ToRectangle(); } } public override void MouseDown(NSEvent mouseEvent) { - if (effect?.IsDisabled ?? true) + if (effect?.Element == null || effect.IsDisabled) return; effect?.HandleUserInteraction(TouchInteractionStatus.Started); effect?.HandleTouch(TouchStatus.Started); + base.MouseDown(mouseEvent); } public override void MouseUp(NSEvent mouseEvent) { - if (effect?.IsDisabled ?? true) + if (effect?.Element == null || effect.IsDisabled) return; if (effect.HoverStatus == HoverStatus.Entered) @@ -160,7 +169,7 @@ public override void MouseUp(NSEvent mouseEvent) public override void MouseDragged(NSEvent mouseEvent) { - if (effect?.IsDisabled ?? true) + if (effect?.Element == null || effect.IsDisabled) return; var status = ViewRect.Contains(mouseEvent.LocationInWindow.ToPoint()) ? TouchStatus.Started : TouchStatus.Canceled; @@ -169,7 +178,7 @@ public override void MouseDragged(NSEvent mouseEvent) (status == TouchStatus.Started && effect.HoverStatus == HoverStatus.Exited)) effect?.HandleHover(status == TouchStatus.Started ? HoverStatus.Entered : HoverStatus.Exited); - if (effect.Status != status) + if (effect?.Status != status) effect?.HandleTouch(status); base.MouseDragged(mouseEvent); @@ -182,6 +191,7 @@ protected override void Dispose(bool disposing) effect = null; container = null; } + base.Dispose(disposing); } } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.tizen.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.tizen.cs index af6746e2d..167078a6c 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.tizen.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.tizen.cs @@ -11,9 +11,9 @@ namespace Xamarin.CommunityToolkit.Tizen.Effects { public class PlatformTouchEffect : PlatformEffect { - GestureLayer gestureLayer; + GestureLayer? gestureLayer; - TouchEffect effect; + TouchEffect? effect; protected override void OnAttached() { @@ -43,20 +43,20 @@ protected override void OnDetached() sealed class TouchTapGestureRecognizer : GestureLayer { - readonly TouchEffect effect; + readonly TouchEffect? effect; bool tapCompleted; bool longTapStarted; public TouchTapGestureRecognizer(EvasObject parent) : base(parent) { - SetTapCallback(GestureType.Tap, GestureLayer.GestureState.Start, OnTapStarted); - SetTapCallback(GestureType.Tap, GestureLayer.GestureState.End, OnGestureEnded); - SetTapCallback(GestureType.Tap, GestureLayer.GestureState.Abort, OnGestureAborted); + SetTapCallback(GestureType.Tap, GestureState.Start, OnTapStarted); + SetTapCallback(GestureType.Tap, GestureState.End, OnGestureEnded); + SetTapCallback(GestureType.Tap, GestureState.Abort, OnGestureAborted); - SetTapCallback(GestureType.LongTap, GestureLayer.GestureState.Start, OnLongTapStarted); - SetTapCallback(GestureType.LongTap, GestureLayer.GestureState.End, OnGestureEnded); - SetTapCallback(GestureType.LongTap, GestureLayer.GestureState.Abort, OnGestureAborted); + SetTapCallback(GestureType.LongTap, GestureState.Start, OnLongTapStarted); + SetTapCallback(GestureType.LongTap, GestureState.End, OnGestureEnded); + SetTapCallback(GestureType.LongTap, GestureState.Abort, OnGestureAborted); } public TouchTapGestureRecognizer(EvasObject parent, TouchEffect effect) @@ -90,10 +90,10 @@ void OnLongTapStarted(TapData data) void OnGestureEnded(TapData data) { - if (effect?.IsDisabled ?? true) + if (effect == null || effect.IsDisabled) return; - HandleTouch(effect?.Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled, TouchInteractionStatus.Completed); + HandleTouch(effect.Status == TouchStatus.Started ? TouchStatus.Completed : TouchStatus.Canceled, TouchInteractionStatus.Completed); IsCanceled = true; tapCompleted = true; } @@ -119,7 +119,7 @@ public void HandleTouch(TouchStatus status, TouchInteractionStatus? touchInterac if (IsCanceled || effect == null) return; - if (effect?.IsDisabled ?? true) + if (effect.IsDisabled) return; if (touchInteractionStatus == TouchInteractionStatus.Started) @@ -128,18 +128,18 @@ public void HandleTouch(TouchStatus status, TouchInteractionStatus? touchInterac touchInteractionStatus = null; } - effect.HandleTouch(status); + effect?.HandleTouch(status); if (touchInteractionStatus.HasValue) - effect.HandleUserInteraction(touchInteractionStatus.Value); + effect?.HandleUserInteraction(touchInteractionStatus.Value); - if (!effect.NativeAnimation) + if (effect == null || !effect.NativeAnimation) return; if (longTapStarted && !tapCompleted) return; var control = effect.Element; - if (!(Platform.GetOrCreateRenderer(control)?.NativeView is Widget nativeView)) + if (Platform.GetOrCreateRenderer(control)?.NativeView is not Widget nativeView) return; if (status == TouchStatus.Started) diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.uwp.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.uwp.cs index 795c7310b..0ce883725 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.uwp.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/PlatformTouchEffect.uwp.cs @@ -14,24 +14,24 @@ namespace Xamarin.CommunityToolkit.UWP.Effects { public class PlatformTouchEffect : PlatformEffect { - const string PointerDownAnimationKey = "PointerDownAnimation"; + const string pointerDownAnimationKey = "PointerDownAnimation"; - const string PointerUpAnimationKey = "PointerUpAnimation"; + const string pointerUpAnimationKey = "PointerUpAnimation"; - TouchEffect effect; + TouchEffect? effect; bool isPressed; bool isIntentionalCaptureLoss; - Storyboard pointerDownStoryboard; + Storyboard? pointerDownStoryboard; - Storyboard pointerUpStoryboard; + Storyboard? pointerUpStoryboard; protected override void OnAttached() { effect = TouchEffect.PickFrom(Element); - if (effect?.IsDisabled ?? true) + if (effect == null || effect.IsDisabled) return; effect.Element = (VisualElement)Element; @@ -41,26 +41,37 @@ protected override void OnAttached() if (string.IsNullOrEmpty(nativeControl.Name)) nativeControl.Name = Guid.NewGuid().ToString(); - if (nativeControl.Resources.ContainsKey(PointerDownAnimationKey)) - pointerDownStoryboard = (Storyboard)nativeControl.Resources[PointerDownAnimationKey]; + if (nativeControl.Resources.ContainsKey(pointerDownAnimationKey)) + { + pointerDownStoryboard = (Storyboard)nativeControl.Resources[pointerDownAnimationKey]; + } else { pointerDownStoryboard = new Storyboard(); var downThemeAnimation = new PointerDownThemeAnimation(); + + Storyboard.SetTargetName(downThemeAnimation, nativeControl.Name); + pointerDownStoryboard.Children.Add(downThemeAnimation); - nativeControl.Resources.Add(new KeyValuePair(PointerDownAnimationKey, pointerDownStoryboard)); + + nativeControl.Resources.Add(new KeyValuePair(pointerDownAnimationKey, pointerDownStoryboard)); } - if (nativeControl.Resources.ContainsKey(PointerUpAnimationKey)) - pointerUpStoryboard = (Storyboard)nativeControl.Resources[PointerUpAnimationKey]; + if (nativeControl.Resources.ContainsKey(pointerUpAnimationKey)) + { + pointerUpStoryboard = (Storyboard)nativeControl.Resources[pointerUpAnimationKey]; + } else { pointerUpStoryboard = new Storyboard(); var upThemeAnimation = new PointerUpThemeAnimation(); + Storyboard.SetTargetName(upThemeAnimation, nativeControl.Name); + pointerUpStoryboard.Children.Add(upThemeAnimation); - nativeControl.Resources.Add(new KeyValuePair(PointerUpAnimationKey, pointerUpStoryboard)); + + nativeControl.Resources.Add(new KeyValuePair(pointerUpAnimationKey, pointerUpStoryboard)); } } @@ -82,6 +93,7 @@ protected override void OnDetached() effect.Element = null; effect = null; + if (Container != null) { Container.PointerPressed -= OnPointerPressed; @@ -95,9 +107,9 @@ protected override void OnDetached() } } - void OnPointerEntered(object sender, PointerRoutedEventArgs e) + void OnPointerEntered(object? sender, PointerRoutedEventArgs e) { - if (effect?.IsDisabled ?? true) + if (effect?.Element == null || effect.IsDisabled) return; effect?.HandleHover(HoverStatus.Entered); @@ -109,9 +121,9 @@ void OnPointerEntered(object sender, PointerRoutedEventArgs e) } } - void OnPointerExited(object sender, PointerRoutedEventArgs e) + void OnPointerExited(object? sender, PointerRoutedEventArgs e) { - if (effect?.IsDisabled ?? true) + if (effect?.Element == null || effect.IsDisabled) return; if (isPressed) @@ -123,21 +135,23 @@ void OnPointerExited(object sender, PointerRoutedEventArgs e) effect?.HandleHover(HoverStatus.Exited); } - void OnPointerCanceled(object sender, PointerRoutedEventArgs e) + void OnPointerCanceled(object? sender, PointerRoutedEventArgs e) { - if (effect?.IsDisabled ?? true) + if (effect?.Element == null || effect.IsDisabled) return; isPressed = false; + effect?.HandleTouch(TouchStatus.Canceled); effect?.HandleUserInteraction(TouchInteractionStatus.Completed); effect?.HandleHover(HoverStatus.Exited); + AnimateTilt(pointerUpStoryboard); } - void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e) + void OnPointerCaptureLost(object? sender, PointerRoutedEventArgs e) { - if (effect?.IsDisabled ?? true) + if (effect?.Element == null || effect.IsDisabled) return; if (isIntentionalCaptureLoss) @@ -145,20 +159,20 @@ void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e) isPressed = false; - if (effect.Status != TouchStatus.Canceled) + if (effect?.Status != TouchStatus.Canceled) effect?.HandleTouch(TouchStatus.Canceled); effect?.HandleUserInteraction(TouchInteractionStatus.Completed); - if (effect.HoverStatus != HoverStatus.Exited) + if (effect?.HoverStatus != HoverStatus.Exited) effect?.HandleHover(HoverStatus.Exited); AnimateTilt(pointerUpStoryboard); } - void OnPointerReleased(object sender, PointerRoutedEventArgs e) + void OnPointerReleased(object? sender, PointerRoutedEventArgs e) { - if (effect?.IsDisabled ?? true) + if (effect?.Element == null || effect.IsDisabled) return; if (isPressed && (effect.HoverStatus == HoverStatus.Entered)) @@ -173,26 +187,31 @@ void OnPointerReleased(object sender, PointerRoutedEventArgs e) } effect?.HandleUserInteraction(TouchInteractionStatus.Completed); + isPressed = false; isIntentionalCaptureLoss = true; } - void OnPointerPressed(object sender, PointerRoutedEventArgs e) + void OnPointerPressed(object? sender, PointerRoutedEventArgs e) { - if (effect?.IsDisabled ?? true) + if (effect?.Element == null || effect.IsDisabled) return; isPressed = true; + Container.CapturePointer(e.Pointer); + effect?.HandleUserInteraction(TouchInteractionStatus.Started); effect?.HandleTouch(TouchStatus.Started); + AnimateTilt(pointerDownStoryboard); + isIntentionalCaptureLoss = false; } - void AnimateTilt(Storyboard storyboard) + void AnimateTilt(Storyboard? storyboard) { - if ((effect?.NativeAnimation ?? false) && storyboard != null) + if (storyboard != null && effect?.Element != null && effect.NativeAnimation) { try { diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/TouchEffect.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/TouchEffect.shared.cs index 9bc825783..b93588e25 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/TouchEffect.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/Touch/TouchEffect.shared.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading.Tasks; using System.Windows.Input; using Xamarin.CommunityToolkit.Helpers; using Xamarin.Forms; @@ -8,6 +9,12 @@ namespace Xamarin.CommunityToolkit.Effects { public class TouchEffect : RoutingEffect { + public const string UnpressedVisualState = "Unpressed"; + + public const string PressedVisualState = "Pressed"; + + public const string HoveredVisualState = "Hovered"; + public event EventHandler StatusChanged { add => weakEventManager.AddEventHandler(value); @@ -44,6 +51,12 @@ public class TouchEffect : RoutingEffect remove => weakEventManager.RemoveEventHandler(value); } + public event EventHandler LongPressCompleted + { + add => weakEventManager.AddEventHandler(value); + remove => weakEventManager.RemoveEventHandler(value); + } + public static readonly BindableProperty IsAvailableProperty = BindableProperty.CreateAttached( nameof(IsAvailable), typeof(bool), @@ -462,7 +475,7 @@ public class TouchEffect : RoutingEffect readonly WeakEventManager weakEventManager = new WeakEventManager(); - VisualElement element; + VisualElement? element; public TouchEffect() : base(EffectIds.TouchEffect) @@ -487,363 +500,423 @@ public TouchEffect() #endregion } - public static bool GetIsAvailable(BindableObject bindable) - => (bool)bindable.GetValue(IsAvailableProperty); + public static bool GetIsAvailable(BindableObject? bindable) + => (bool)(bindable?.GetValue(IsAvailableProperty) ?? throw new ArgumentNullException(nameof(bindable))); + + public static void SetIsAvailable(BindableObject? bindable, bool value) + => bindable?.SetValue(IsAvailableProperty, value); + + public static bool GetShouldMakeChildrenInputTransparent(BindableObject? bindable) + => (bool)(bindable?.GetValue(ShouldMakeChildrenInputTransparentProperty) ?? throw new ArgumentNullException(nameof(bindable))); + + public static void SetShouldMakeChildrenInputTransparent(BindableObject? bindable, bool value) + => bindable?.SetValue(ShouldMakeChildrenInputTransparentProperty, value); + + public static ICommand? GetCommand(BindableObject? bindable) + { + if (bindable == null) + throw new ArgumentNullException(nameof(bindable)); + + return (ICommand?)bindable.GetValue(CommandProperty); + } + + public static void SetCommand(BindableObject? bindable, ICommand value) + => bindable?.SetValue(CommandProperty, value); + + public static ICommand? GetLongPressCommand(BindableObject? bindable) + { + if (bindable == null) + throw new ArgumentNullException(nameof(bindable)); + + return (ICommand?)bindable.GetValue(LongPressCommandProperty); + } + + public static void SetLongPressCommand(BindableObject? bindable, ICommand value) + => bindable?.SetValue(LongPressCommandProperty, value); + + public static object? GetCommandParameter(BindableObject? bindable) + { + if (bindable == null) + throw new ArgumentNullException(nameof(bindable)); + + return bindable.GetValue(CommandParameterProperty); + } + + public static void SetCommandParameter(BindableObject? bindable, object value) + => bindable?.SetValue(CommandParameterProperty, value); - public static void SetIsAvailable(BindableObject bindable, bool value) - => bindable.SetValue(IsAvailableProperty, value); + public static object? GetLongPressCommandParameter(BindableObject? bindable) + { + if (bindable == null) + throw new ArgumentNullException(nameof(bindable)); - public static bool GetShouldMakeChildrenInputTransparent(BindableObject bindable) - => (bool)bindable.GetValue(ShouldMakeChildrenInputTransparentProperty); + return bindable.GetValue(LongPressCommandParameterProperty); + } - public static void SetShouldMakeChildrenInputTransparent(BindableObject bindable, bool value) - => bindable.SetValue(ShouldMakeChildrenInputTransparentProperty, value); + public static void SetLongPressCommandParameter(BindableObject? bindable, object value) + => bindable?.SetValue(LongPressCommandParameterProperty, value); - public static ICommand GetCommand(BindableObject bindable) - => bindable.GetValue(CommandProperty) as ICommand; + public static int GetLongPressDuration(BindableObject? bindable) + => (int)(bindable?.GetValue(LongPressDurationProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetCommand(BindableObject bindable, ICommand value) - => bindable.SetValue(CommandProperty, value); + public static void SetLongPressDuration(BindableObject? bindable, int value) + => bindable?.SetValue(LongPressDurationProperty, value); - public static ICommand GetLongPressCommand(BindableObject bindable) - => bindable.GetValue(LongPressCommandProperty) as ICommand; + public static TouchStatus GetStatus(BindableObject? bindable) + => (TouchStatus)(bindable?.GetValue(StatusProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetLongPressCommand(BindableObject bindable, ICommand value) - => bindable.SetValue(LongPressCommandProperty, value); + public static void SetStatus(BindableObject? bindable, TouchStatus value) + => bindable?.SetValue(StatusProperty, value); - public static object GetCommandParameter(BindableObject bindable) - => bindable.GetValue(CommandParameterProperty); + public static TouchState GetState(BindableObject? bindable) + => (TouchState)(bindable?.GetValue(StateProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetCommandParameter(BindableObject bindable, object value) - => bindable.SetValue(CommandParameterProperty, value); + public static void SetState(BindableObject? bindable, TouchState value) + => bindable?.SetValue(StateProperty, value); - public static object GetLongPressCommandParameter(BindableObject bindable) - => bindable.GetValue(LongPressCommandParameterProperty); + public static TouchInteractionStatus GetInteractionStatus(BindableObject? bindable) + => (TouchInteractionStatus)(bindable?.GetValue(InteractionStatusProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetLongPressCommandParameter(BindableObject bindable, object value) - => bindable.SetValue(LongPressCommandParameterProperty, value); + public static void SetInteractionStatus(BindableObject? bindable, TouchInteractionStatus value) + => bindable?.SetValue(InteractionStatusProperty, value); - public static int GetLongPressDuration(BindableObject bindable) - => (int)bindable.GetValue(LongPressDurationProperty); + public static HoverStatus GetHoverStatus(BindableObject? bindable) + => (HoverStatus)(bindable?.GetValue(HoverStatusProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetLongPressDuration(BindableObject bindable, int value) - => bindable.SetValue(LongPressDurationProperty, value); + public static void SetHoverStatus(BindableObject? bindable, HoverStatus value) + => bindable?.SetValue(HoverStatusProperty, value); - public static TouchStatus GetStatus(BindableObject bindable) - => (TouchStatus)bindable.GetValue(StatusProperty); + public static HoverState GetHoverState(BindableObject? bindable) + => (HoverState)(bindable?.GetValue(HoverStateProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetStatus(BindableObject bindable, TouchStatus value) - => bindable.SetValue(StatusProperty, value); + public static void SetHoverState(BindableObject? bindable, HoverState value) + => bindable?.SetValue(HoverStateProperty, value); - public static TouchState GetState(BindableObject bindable) - => (TouchState)bindable.GetValue(StateProperty); + public static Color GetNormalBackgroundColor(BindableObject? bindable) + => (Color)(bindable?.GetValue(NormalBackgroundColorProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetState(BindableObject bindable, TouchState value) - => bindable.SetValue(StateProperty, value); + public static void SetNormalBackgroundColor(BindableObject? bindable, Color value) + => bindable?.SetValue(NormalBackgroundColorProperty, value); - public static TouchInteractionStatus GetInteractionStatus(BindableObject bindable) - => (TouchInteractionStatus)bindable.GetValue(InteractionStatusProperty); + public static Color GetHoveredBackgroundColor(BindableObject? bindable) + => (Color)(bindable?.GetValue(HoveredBackgroundColorProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetInteractionStatus(BindableObject bindable, TouchInteractionStatus value) - => bindable.SetValue(InteractionStatusProperty, value); + public static void SetHoveredBackgroundColor(BindableObject? bindable, Color value) + => bindable?.SetValue(HoveredBackgroundColorProperty, value); - public static HoverStatus GetHoverStatus(BindableObject bindable) - => (HoverStatus)bindable.GetValue(HoverStatusProperty); + public static Color GetPressedBackgroundColor(BindableObject? bindable) + => (Color)(bindable?.GetValue(PressedBackgroundColorProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetHoverStatus(BindableObject bindable, HoverStatus value) - => bindable.SetValue(HoverStatusProperty, value); + public static void SetPressedBackgroundColor(BindableObject? bindable, Color value) + => bindable?.SetValue(PressedBackgroundColorProperty, value); - public static HoverState GetHoverState(BindableObject bindable) - => (HoverState)bindable.GetValue(HoverStateProperty); + public static double GetNormalOpacity(BindableObject? bindable) + => (double)(bindable?.GetValue(NormalOpacityProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetHoverState(BindableObject bindable, HoverState value) - => bindable.SetValue(HoverStateProperty, value); + public static void SetNormalOpacity(BindableObject? bindable, double value) + => bindable?.SetValue(NormalOpacityProperty, value); - public static Color GetNormalBackgroundColor(BindableObject bindable) - => (Color)bindable.GetValue(NormalBackgroundColorProperty); + public static double GetHoveredOpacity(BindableObject? bindable) + => (double)(bindable?.GetValue(HoveredOpacityProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetNormalBackgroundColor(BindableObject bindable, Color value) - => bindable.SetValue(NormalBackgroundColorProperty, value); + public static void SetHoveredOpacity(BindableObject? bindable, double value) + => bindable?.SetValue(HoveredOpacityProperty, value); - public static Color GetHoveredBackgroundColor(BindableObject bindable) - => (Color)bindable.GetValue(HoveredBackgroundColorProperty); + public static double GetPressedOpacity(BindableObject? bindable) + => (double)(bindable?.GetValue(PressedOpacityProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetHoveredBackgroundColor(BindableObject bindable, Color value) - => bindable.SetValue(HoveredBackgroundColorProperty, value); + public static void SetPressedOpacity(BindableObject? bindable, double value) + => bindable?.SetValue(PressedOpacityProperty, value); - public static Color GetPressedBackgroundColor(BindableObject bindable) - => (Color)bindable.GetValue(PressedBackgroundColorProperty); + public static double GetNormalScale(BindableObject? bindable) + => (double)(bindable?.GetValue(NormalScaleProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetPressedBackgroundColor(BindableObject bindable, Color value) - => bindable.SetValue(PressedBackgroundColorProperty, value); + public static void SetNormalScale(BindableObject? bindable, double value) + => bindable?.SetValue(NormalScaleProperty, value); - public static double GetNormalOpacity(BindableObject bindable) - => (double)bindable.GetValue(NormalOpacityProperty); + public static double GetHoveredScale(BindableObject? bindable) + => (double)(bindable?.GetValue(HoveredScaleProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetNormalOpacity(BindableObject bindable, double value) - => bindable.SetValue(NormalOpacityProperty, value); + public static void SetHoveredScale(BindableObject? bindable, double value) + => bindable?.SetValue(HoveredScaleProperty, value); - public static double GetHoveredOpacity(BindableObject bindable) - => (double)bindable.GetValue(HoveredOpacityProperty); + public static double GetPressedScale(BindableObject? bindable) + => (double)(bindable?.GetValue(PressedScaleProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetHoveredOpacity(BindableObject bindable, double value) - => bindable.SetValue(HoveredOpacityProperty, value); + public static void SetPressedScale(BindableObject? bindable, double value) + => bindable?.SetValue(PressedScaleProperty, value); - public static double GetPressedOpacity(BindableObject bindable) - => (double)bindable.GetValue(PressedOpacityProperty); + public static double GetNormalTranslationX(BindableObject? bindable) + => (double)(bindable?.GetValue(NormalTranslationXProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetPressedOpacity(BindableObject bindable, double value) - => bindable.SetValue(PressedOpacityProperty, value); + public static void SetNormalTranslationX(BindableObject? bindable, double value) + => bindable?.SetValue(NormalTranslationXProperty, value); - public static double GetNormalScale(BindableObject bindable) - => (double)bindable.GetValue(NormalScaleProperty); + public static double GetHoveredTranslationX(BindableObject? bindable) + => (double)(bindable?.GetValue(HoveredTranslationXProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetNormalScale(BindableObject bindable, double value) - => bindable.SetValue(NormalScaleProperty, value); + public static void SetHoveredTranslationX(BindableObject? bindable, double value) + => bindable?.SetValue(HoveredTranslationXProperty, value); - public static double GetHoveredScale(BindableObject bindable) - => (double)bindable.GetValue(HoveredScaleProperty); + public static double GetPressedTranslationX(BindableObject? bindable) + => (double)(bindable?.GetValue(PressedTranslationXProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetHoveredScale(BindableObject bindable, double value) - => bindable.SetValue(HoveredScaleProperty, value); + public static void SetPressedTranslationX(BindableObject? bindable, double value) + => bindable?.SetValue(PressedTranslationXProperty, value); - public static double GetPressedScale(BindableObject bindable) - => (double)bindable.GetValue(PressedScaleProperty); + public static double GetNormalTranslationY(BindableObject? bindable) + => (double)(bindable?.GetValue(NormalTranslationYProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetPressedScale(BindableObject bindable, double value) - => bindable.SetValue(PressedScaleProperty, value); + public static void SetNormalTranslationY(BindableObject? bindable, double value) + => bindable?.SetValue(NormalTranslationYProperty, value); - public static double GetNormalTranslationX(BindableObject bindable) - => (double)bindable.GetValue(NormalTranslationXProperty); + public static double GetHoveredTranslationY(BindableObject? bindable) + => (double)(bindable?.GetValue(HoveredTranslationYProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetNormalTranslationX(BindableObject bindable, double value) - => bindable.SetValue(NormalTranslationXProperty, value); + public static void SetHoveredTranslationY(BindableObject? bindable, double value) + => bindable?.SetValue(HoveredTranslationYProperty, value); - public static double GetHoveredTranslationX(BindableObject bindable) - => (double)bindable.GetValue(HoveredTranslationXProperty); + public static double GetPressedTranslationY(BindableObject? bindable) + => (double)(bindable?.GetValue(PressedTranslationYProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetHoveredTranslationX(BindableObject bindable, double value) - => bindable.SetValue(HoveredTranslationXProperty, value); + public static void SetPressedTranslationY(BindableObject? bindable, double value) + => bindable?.SetValue(PressedTranslationYProperty, value); - public static double GetPressedTranslationX(BindableObject bindable) - => (double)bindable.GetValue(PressedTranslationXProperty); + public static double GetNormalRotation(BindableObject? bindable) + => (double)(bindable?.GetValue(NormalRotationProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetPressedTranslationX(BindableObject bindable, double value) - => bindable.SetValue(PressedTranslationXProperty, value); + public static void SetNormalRotation(BindableObject? bindable, double value) + => bindable?.SetValue(NormalRotationProperty, value); - public static double GetNormalTranslationY(BindableObject bindable) - => (double)bindable.GetValue(NormalTranslationYProperty); + public static double GetHoveredRotation(BindableObject? bindable) + => (double)(bindable?.GetValue(HoveredRotationProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetNormalTranslationY(BindableObject bindable, double value) - => bindable.SetValue(NormalTranslationYProperty, value); + public static void SetHoveredRotation(BindableObject? bindable, double value) + => bindable?.SetValue(HoveredRotationProperty, value); - public static double GetHoveredTranslationY(BindableObject bindable) - => (double)bindable.GetValue(HoveredTranslationYProperty); + public static double GetPressedRotation(BindableObject? bindable) + => (double)(bindable?.GetValue(PressedRotationProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetHoveredTranslationY(BindableObject bindable, double value) - => bindable.SetValue(HoveredTranslationYProperty, value); + public static void SetPressedRotation(BindableObject? bindable, double value) + => bindable?.SetValue(PressedRotationProperty, value); - public static double GetPressedTranslationY(BindableObject bindable) - => (double)bindable.GetValue(PressedTranslationYProperty); + public static double GetNormalRotationX(BindableObject? bindable) + => (double)(bindable?.GetValue(NormalRotationXProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetPressedTranslationY(BindableObject bindable, double value) - => bindable.SetValue(PressedTranslationYProperty, value); + public static void SetNormalRotationX(BindableObject? bindable, double value) + => bindable?.SetValue(NormalRotationXProperty, value); - public static double GetNormalRotation(BindableObject bindable) - => (double)bindable.GetValue(NormalRotationProperty); + public static double GetHoveredRotationX(BindableObject? bindable) + => (double)(bindable?.GetValue(HoveredRotationXProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetNormalRotation(BindableObject bindable, double value) - => bindable.SetValue(NormalRotationProperty, value); + public static void SetHoveredRotationX(BindableObject? bindable, double value) + => bindable?.SetValue(HoveredRotationXProperty, value); - public static double GetHoveredRotation(BindableObject bindable) - => (double)bindable.GetValue(HoveredRotationProperty); + public static double GetPressedRotationX(BindableObject? bindable) + => (double)(bindable?.GetValue(PressedRotationXProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetHoveredRotation(BindableObject bindable, double value) - => bindable.SetValue(HoveredRotationProperty, value); + public static void SetPressedRotationX(BindableObject? bindable, double value) + => bindable?.SetValue(PressedRotationXProperty, value); - public static double GetPressedRotation(BindableObject bindable) - => (double)bindable.GetValue(PressedRotationProperty); + public static double GetNormalRotationY(BindableObject? bindable) + => (double)(bindable?.GetValue(NormalRotationYProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetPressedRotation(BindableObject bindable, double value) - => bindable.SetValue(PressedRotationProperty, value); + public static void SetNormalRotationY(BindableObject? bindable, double value) + => bindable?.SetValue(NormalRotationYProperty, value); - public static double GetNormalRotationX(BindableObject bindable) - => (double)bindable.GetValue(NormalRotationXProperty); + public static double GetHoveredRotationY(BindableObject? bindable) + => (double)(bindable?.GetValue(HoveredRotationYProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetNormalRotationX(BindableObject bindable, double value) - => bindable.SetValue(NormalRotationXProperty, value); + public static void SetHoveredRotationY(BindableObject? bindable, double value) + => bindable?.SetValue(HoveredRotationYProperty, value); - public static double GetHoveredRotationX(BindableObject bindable) - => (double)bindable.GetValue(HoveredRotationXProperty); + public static double GetPressedRotationY(BindableObject? bindable) + => (double)(bindable?.GetValue(PressedRotationYProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetHoveredRotationX(BindableObject bindable, double value) - => bindable.SetValue(HoveredRotationXProperty, value); + public static void SetPressedRotationY(BindableObject? bindable, double value) + => bindable?.SetValue(PressedRotationYProperty, value); - public static double GetPressedRotationX(BindableObject bindable) - => (double)bindable.GetValue(PressedRotationXProperty); + public static int GetAnimationDuration(BindableObject? bindable) + => (int)(bindable?.GetValue(AnimationDurationProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetPressedRotationX(BindableObject bindable, double value) - => bindable.SetValue(PressedRotationXProperty, value); + public static void SetAnimationDuration(BindableObject? bindable, int value) + => bindable?.SetValue(AnimationDurationProperty, value); - public static double GetNormalRotationY(BindableObject bindable) - => (double)bindable.GetValue(NormalRotationYProperty); + public static Easing? GetAnimationEasing(BindableObject? bindable) + { + if (bindable == null) + throw new ArgumentNullException(nameof(bindable)); - public static void SetNormalRotationY(BindableObject bindable, double value) - => bindable.SetValue(NormalRotationYProperty, value); + return (Easing?)bindable.GetValue(AnimationEasingProperty); + } - public static double GetHoveredRotationY(BindableObject bindable) - => (double)bindable.GetValue(HoveredRotationYProperty); + public static void SetAnimationEasing(BindableObject? bindable, Easing? value) + => bindable?.SetValue(AnimationEasingProperty, value); - public static void SetHoveredRotationY(BindableObject bindable, double value) - => bindable.SetValue(HoveredRotationYProperty, value); + public static int GetPressedAnimationDuration(BindableObject? bindable) + => (int)(bindable?.GetValue(PressedAnimationDurationProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static double GetPressedRotationY(BindableObject bindable) - => (double)bindable.GetValue(PressedRotationYProperty); + public static void SetPressedAnimationDuration(BindableObject? bindable, int value) + => bindable?.SetValue(PressedAnimationDurationProperty, value); - public static void SetPressedRotationY(BindableObject bindable, double value) - => bindable.SetValue(PressedRotationYProperty, value); + public static Easing? GetPressedAnimationEasing(BindableObject? bindable) + { + if (bindable == null) + throw new ArgumentNullException(nameof(bindable)); - public static int GetAnimationDuration(BindableObject bindable) - => (int)bindable.GetValue(AnimationDurationProperty); + return (Easing?)bindable.GetValue(PressedAnimationEasingProperty); + } - public static void SetAnimationDuration(BindableObject bindable, int value) - => bindable.SetValue(AnimationDurationProperty, value); + public static void SetPressedAnimationEasing(BindableObject? bindable, Easing? value) + => bindable?.SetValue(PressedAnimationEasingProperty, value); - public static Easing GetAnimationEasing(BindableObject bindable) - => bindable.GetValue(AnimationEasingProperty) as Easing; + public static int GetNormalAnimationDuration(BindableObject? bindable) + => (int)(bindable?.GetValue(NormalAnimationDurationProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetAnimationEasing(BindableObject bindable, Easing value) - => bindable.SetValue(AnimationEasingProperty, value); + public static void SetNormalAnimationDuration(BindableObject? bindable, int value) + => bindable?.SetValue(NormalAnimationDurationProperty, value); - public static int GetPressedAnimationDuration(BindableObject bindable) - => (int)bindable.GetValue(PressedAnimationDurationProperty); + public static Easing? GetNormalAnimationEasing(BindableObject? bindable) + { + if (bindable == null) + throw new ArgumentNullException(nameof(bindable)); - public static void SetPressedAnimationDuration(BindableObject bindable, int value) - => bindable.SetValue(PressedAnimationDurationProperty, value); + return (Easing?)bindable.GetValue(NormalAnimationEasingProperty); + } - public static Easing GetPressedAnimationEasing(BindableObject bindable) - => bindable.GetValue(PressedAnimationEasingProperty) as Easing; + public static void SetNormalAnimationEasing(BindableObject? bindable, Easing? value) + => bindable?.SetValue(NormalAnimationEasingProperty, value); - public static void SetPressedAnimationEasing(BindableObject bindable, Easing value) - => bindable.SetValue(PressedAnimationEasingProperty, value); + public static int GetHoveredAnimationDuration(BindableObject? bindable) + => (int)(bindable?.GetValue(HoveredAnimationDurationProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static int GetNormalAnimationDuration(BindableObject bindable) - => (int)bindable.GetValue(NormalAnimationDurationProperty); + public static void SetHoveredAnimationDuration(BindableObject? bindable, int value) + => bindable?.SetValue(HoveredAnimationDurationProperty, value); - public static void SetNormalAnimationDuration(BindableObject bindable, int value) - => bindable.SetValue(NormalAnimationDurationProperty, value); + public static Easing? GetHoveredAnimationEasing(BindableObject? bindable) + { + if (bindable == null) + throw new ArgumentNullException(nameof(bindable)); - public static Easing GetNormalAnimationEasing(BindableObject bindable) - => bindable.GetValue(NormalAnimationEasingProperty) as Easing; + return (Easing?)bindable.GetValue(HoveredAnimationEasingProperty); + } - public static void SetNormalAnimationEasing(BindableObject bindable, Easing value) - => bindable.SetValue(NormalAnimationEasingProperty, value); + public static void SetHoveredAnimationEasing(BindableObject? bindable, Easing? value) + => bindable?.SetValue(HoveredAnimationEasingProperty, value); - public static int GetHoveredAnimationDuration(BindableObject bindable) - => (int)bindable.GetValue(HoveredAnimationDurationProperty); + public static int GetPulseCount(BindableObject? bindable) + => (int)(bindable?.GetValue(PulseCountProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetHoveredAnimationDuration(BindableObject bindable, int value) - => bindable.SetValue(HoveredAnimationDurationProperty, value); + public static void SetPulseCount(BindableObject? bindable, int value) + => bindable?.SetValue(PulseCountProperty, value); - public static Easing GetHoveredAnimationEasing(BindableObject bindable) - => bindable.GetValue(HoveredAnimationEasingProperty) as Easing; + public static bool? GetIsToggled(BindableObject? bindable) + { + if (bindable == null) + throw new ArgumentNullException(nameof(bindable)); - public static void SetHoveredAnimationEasing(BindableObject bindable, Easing value) - => bindable.SetValue(HoveredAnimationEasingProperty, value); + return (bool?)bindable.GetValue(IsToggledProperty); + } - public static int GetPulseCount(BindableObject bindable) - => (int)bindable.GetValue(PulseCountProperty); + public static void SetIsToggled(BindableObject? bindable, bool? value) + => bindable?.SetValue(IsToggledProperty, value); - public static void SetPulseCount(BindableObject bindable, int value) - => bindable.SetValue(PulseCountProperty, value); + public static int GetDisallowTouchThreshold(BindableObject? bindable) + => (int)(bindable?.GetValue(DisallowTouchThresholdProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static bool? GetIsToggled(BindableObject bindable) - => (bool?)bindable.GetValue(IsToggledProperty); + public static void SetDisallowTouchThreshold(BindableObject? bindable, int value) + => bindable?.SetValue(DisallowTouchThresholdProperty, value); - public static void SetIsToggled(BindableObject bindable, bool? value) - => bindable.SetValue(IsToggledProperty, value); + public static bool GetNativeAnimation(BindableObject? bindable) + => (bool)(bindable?.GetValue(NativeAnimationProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static int GetDisallowTouchThreshold(BindableObject bindable) - => (int)bindable.GetValue(DisallowTouchThresholdProperty); + public static void SetNativeAnimation(BindableObject? bindable, bool value) + => bindable?.SetValue(NativeAnimationProperty, value); - public static void SetDisallowTouchThreshold(BindableObject bindable, int value) - => bindable.SetValue(DisallowTouchThresholdProperty, value); + public static Color GetNativeAnimationColor(BindableObject? bindable) + => (Color)(bindable?.GetValue(NativeAnimationColorProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static bool GetNativeAnimation(BindableObject bindable) - => (bool)bindable.GetValue(NativeAnimationProperty); + public static void SetNativeAnimationColor(BindableObject? bindable, Color value) + => bindable?.SetValue(NativeAnimationColorProperty, value); - public static void SetNativeAnimation(BindableObject bindable, bool value) - => bindable.SetValue(NativeAnimationProperty, value); + public static int GetNativeAnimationRadius(BindableObject? bindable) + => (int)(bindable?.GetValue(NativeAnimationRadiusProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static Color GetNativeAnimationColor(BindableObject bindable) - => (Color)bindable.GetValue(NativeAnimationColorProperty); + public static void SetNativeAnimationRadius(BindableObject? bindable, int value) + => bindable?.SetValue(NativeAnimationRadiusProperty, value); - public static void SetNativeAnimationColor(BindableObject bindable, Color value) - => bindable.SetValue(NativeAnimationColorProperty, value); + public static int GetNativeAnimationShadowRadius(BindableObject? bindable) + => (int)(bindable?.GetValue(NativeAnimationShadowRadiusProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static int GetNativeAnimationRadius(BindableObject bindable) - => (int)bindable.GetValue(NativeAnimationRadiusProperty); + public static void SetNativeAnimationShadowRadius(BindableObject? bindable, int value) + => bindable?.SetValue(NativeAnimationShadowRadiusProperty, value); - public static void SetNativeAnimationRadius(BindableObject bindable, int value) - => bindable.SetValue(NativeAnimationRadiusProperty, value); + public static ImageSource? GetNormalBackgroundImageSource(BindableObject? bindable) + { + if (bindable == null) + throw new ArgumentNullException(nameof(bindable)); - public static int GetNativeAnimationShadowRadius(BindableObject bindable) - => (int)bindable.GetValue(NativeAnimationShadowRadiusProperty); + return (ImageSource?)bindable.GetValue(NormalBackgroundImageSourceProperty); + } - public static void SetNativeAnimationShadowRadius(BindableObject bindable, int value) - => bindable.SetValue(NativeAnimationShadowRadiusProperty, value); + public static void SetNormalBackgroundImageSource(BindableObject? bindable, ImageSource value) + => bindable?.SetValue(NormalBackgroundImageSourceProperty, value); - public static ImageSource GetNormalBackgroundImageSource(BindableObject bindable) - => (ImageSource)bindable.GetValue(NormalBackgroundImageSourceProperty); + public static ImageSource? GetHoveredBackgroundImageSource(BindableObject? bindable) + { + if (bindable == null) + throw new ArgumentNullException(nameof(bindable)); - public static void SetNormalBackgroundImageSource(BindableObject bindable, ImageSource value) - => bindable.SetValue(NormalBackgroundImageSourceProperty, value); + return (ImageSource?)bindable.GetValue(HoveredBackgroundImageSourceProperty); + } - public static ImageSource GetHoveredBackgroundImageSource(BindableObject bindable) - => (ImageSource)bindable.GetValue(HoveredBackgroundImageSourceProperty); + public static void SetHoveredBackgroundImageSource(BindableObject? bindable, ImageSource value) + => bindable?.SetValue(HoveredBackgroundImageSourceProperty, value); - public static void SetHoveredBackgroundImageSource(BindableObject bindable, ImageSource value) - => bindable.SetValue(HoveredBackgroundImageSourceProperty, value); + public static ImageSource? GetPressedBackgroundImageSource(BindableObject? bindable) + { + if (bindable == null) + throw new ArgumentNullException(nameof(bindable)); - public static ImageSource GetPressedBackgroundImageSource(BindableObject bindable) - => (ImageSource)bindable.GetValue(PressedBackgroundImageSourceProperty); + return (ImageSource?)bindable.GetValue(PressedBackgroundImageSourceProperty); + } - public static void SetPressedBackgroundImageSource(BindableObject bindable, ImageSource value) - => bindable.SetValue(PressedBackgroundImageSourceProperty, value); + public static void SetPressedBackgroundImageSource(BindableObject? bindable, ImageSource value) + => bindable?.SetValue(PressedBackgroundImageSourceProperty, value); - public static Aspect GetBackgroundImageAspect(BindableObject bindable) - => (Aspect)bindable.GetValue(BackgroundImageAspectProperty); + public static Aspect GetBackgroundImageAspect(BindableObject? bindable) + => (Aspect)(bindable?.GetValue(BackgroundImageAspectProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetBackgroundImageAspect(BindableObject bindable, Aspect value) - => bindable.SetValue(BackgroundImageAspectProperty, value); + public static void SetBackgroundImageAspect(BindableObject? bindable, Aspect value) + => bindable?.SetValue(BackgroundImageAspectProperty, value); - public static Aspect GetNormalBackgroundImageAspect(BindableObject bindable) - => (Aspect)bindable.GetValue(NormalBackgroundImageAspectProperty); + public static Aspect GetNormalBackgroundImageAspect(BindableObject? bindable) + => (Aspect)(bindable?.GetValue(NormalBackgroundImageAspectProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetNormalBackgroundImageAspect(BindableObject bindable, Aspect value) - => bindable.SetValue(NormalBackgroundImageAspectProperty, value); + public static void SetNormalBackgroundImageAspect(BindableObject? bindable, Aspect value) + => bindable?.SetValue(NormalBackgroundImageAspectProperty, value); - public static Aspect GetHoveredBackgroundImageAspect(BindableObject bindable) - => (Aspect)bindable.GetValue(HoveredBackgroundImageAspectProperty); + public static Aspect GetHoveredBackgroundImageAspect(BindableObject? bindable) + => (Aspect)(bindable?.GetValue(HoveredBackgroundImageAspectProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetHoveredBackgroundImageAspect(BindableObject bindable, Aspect value) - => bindable.SetValue(HoveredBackgroundImageAspectProperty, value); + public static void SetHoveredBackgroundImageAspect(BindableObject? bindable, Aspect value) + => bindable?.SetValue(HoveredBackgroundImageAspectProperty, value); - public static Aspect GetPressedBackgroundImageAspect(BindableObject bindable) - => (Aspect)bindable.GetValue(PressedBackgroundImageAspectProperty); + public static Aspect GetPressedBackgroundImageAspect(BindableObject? bindable) + => (Aspect)(bindable?.GetValue(PressedBackgroundImageAspectProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetPressedBackgroundImageAspect(BindableObject bindable, Aspect value) - => bindable.SetValue(PressedBackgroundImageAspectProperty, value); + public static void SetPressedBackgroundImageAspect(BindableObject? bindable, Aspect value) + => bindable?.SetValue(PressedBackgroundImageAspectProperty, value); - public static bool GetShouldSetImageOnAnimationEnd(BindableObject bindable) - => (bool)bindable.GetValue(ShouldSetImageOnAnimationEndProperty); + public static bool GetShouldSetImageOnAnimationEnd(BindableObject? bindable) + => (bool)(bindable?.GetValue(ShouldSetImageOnAnimationEndProperty) ?? throw new ArgumentNullException(nameof(bindable))); - public static void SetShouldSetImageOnAnimationEnd(BindableObject bindable, bool value) - => bindable.SetValue(ShouldSetImageOnAnimationEndProperty, value); + public static void SetShouldSetImageOnAnimationEnd(BindableObject? bindable, bool value) + => bindable?.SetValue(ShouldSetImageOnAnimationEndProperty, value); - static void TryGenerateEffect(BindableObject bindable, object oldValue, object newValue) + static void TryGenerateEffect(BindableObject? bindable, object oldValue, object newValue) { - if (!(bindable is VisualElement view) || view.Effects.OfType().Any()) + if (bindable is not VisualElement view || view.Effects.OfType().Any()) return; view.Effects.Add(new TouchEffect { IsAutoGenerated = true }); @@ -857,7 +930,7 @@ static void ForceUpdateStateAndTryGenerateEffect(BindableObject bindable, object static void ForceUpdateStateWithoutAnimationAndTryGenerateEffect(BindableObject bindable, object oldValue, object newValue) { - GetFrom(bindable)?.ForceUpdateState(false); + GetFrom(bindable)?.ForceUpdateState(); TryGenerateEffect(bindable, oldValue, newValue); } @@ -877,13 +950,13 @@ static void SetChildrenInputTransparentAndTryGenerateEffect(BindableObject binda public bool ShouldMakeChildrenInputTransparent => GetShouldMakeChildrenInputTransparent(Element); - public ICommand Command => GetCommand(Element); + public ICommand? Command => GetCommand(Element); - public ICommand LongPressCommand => GetLongPressCommand(Element); + public ICommand? LongPressCommand => GetLongPressCommand(Element); - public object CommandParameter => GetCommandParameter(Element); + public object? CommandParameter => GetCommandParameter(Element); - public object LongPressCommandParameter => GetLongPressCommandParameter(Element); + public object? LongPressCommandParameter => GetLongPressCommandParameter(Element); public int LongPressDuration => GetLongPressDuration(Element); @@ -977,19 +1050,19 @@ public HoverState HoverState public int AnimationDuration => GetAnimationDuration(Element); - public Easing AnimationEasing => GetAnimationEasing(Element); + public Easing? AnimationEasing => GetAnimationEasing(Element); public int PressedAnimationDuration => GetPressedAnimationDuration(Element); - public Easing PressedAnimationEasing => GetPressedAnimationEasing(Element); + public Easing? PressedAnimationEasing => GetPressedAnimationEasing(Element); public int NormalAnimationDuration => GetNormalAnimationDuration(Element); - public Easing NormalAnimationEasing => GetNormalAnimationEasing(Element); + public Easing? NormalAnimationEasing => GetNormalAnimationEasing(Element); public int HoveredAnimationDuration => GetHoveredAnimationDuration(Element); - public Easing HoveredAnimationEasing => GetHoveredAnimationEasing(Element); + public Easing? HoveredAnimationEasing => GetHoveredAnimationEasing(Element); public int PulseCount => GetPulseCount(Element); @@ -999,11 +1072,11 @@ public HoverState HoverState internal set => SetIsToggled(Element, value); } - public ImageSource NormalBackgroundImageSource => GetNormalBackgroundImageSource(Element); + public ImageSource? NormalBackgroundImageSource => GetNormalBackgroundImageSource(Element); - public ImageSource HoveredBackgroundImageSource => GetHoveredBackgroundImageSource(Element); + public ImageSource? HoveredBackgroundImageSource => GetHoveredBackgroundImageSource(Element); - public ImageSource PressedBackgroundImageSource => GetPressedBackgroundImageSource(Element); + public ImageSource? PressedBackgroundImageSource => GetPressedBackgroundImageSource(Element); public Aspect BackgroundImageAspect => GetBackgroundImageAspect(Element); @@ -1015,12 +1088,11 @@ public HoverState HoverState public bool ShouldSetImageOnAnimationEnd => GetShouldSetImageOnAnimationEnd(Element); - internal bool CanExecute - => IsAvailable && - Element.IsEnabled && - (Command?.CanExecute(CommandParameter) ?? true); + internal bool CanExecute => IsAvailable + && (Element?.IsEnabled ?? false) + && (Command?.CanExecute(CommandParameter) ?? true); - internal new VisualElement Element + internal new VisualElement? Element { get => element; set @@ -1042,19 +1114,19 @@ internal new VisualElement Element foreach (var effect in value.Effects.OfType()) effect.IsDisabled = effect != this; } - ForceUpdateState(false); + + ForceUpdateState(); } } } - internal static TouchEffect GetFrom(BindableObject bindable) + internal static TouchEffect? GetFrom(BindableObject? bindable) { var effects = (bindable as VisualElement)?.Effects?.OfType(); - return effects?.FirstOrDefault(x => !x.IsAutoGenerated) - ?? effects?.FirstOrDefault(); + return effects?.FirstOrDefault(x => !x.IsAutoGenerated) ?? effects?.FirstOrDefault(); } - internal static TouchEffect PickFrom(BindableObject bindable) + internal static TouchEffect? PickFrom(BindableObject? bindable) { var effects = (bindable as VisualElement)?.Effects?.OfType(); return effects?.FirstOrDefault(x => !x.IsAutoGenerated && !x.IsUsed) @@ -1094,14 +1166,27 @@ internal void RaiseHoverStatusChanged() => weakEventManager.RaiseEvent(Element, new HoverStatusChangedEventArgs(HoverStatus), nameof(HoverStatusChanged)); internal void RaiseCompleted() - => weakEventManager.RaiseEvent(Element, new TouchCompletedEventArgs(CommandParameter), nameof(Completed)); + { + var element = Element; + var parameter = CommandParameter; + Command?.Execute(parameter); + weakEventManager.RaiseEvent(element, new TouchCompletedEventArgs(parameter), nameof(Completed)); + } + + internal void RaiseLongPressCompleted() + { + var element = Element; + var parameter = LongPressCommandParameter ?? CommandParameter; + LongPressCommand?.Execute(parameter); + weakEventManager.RaiseEvent(element, new LongPressCompletedEventArgs(parameter), nameof(LongPressCompleted)); + } internal void ForceUpdateState(bool animated = true) { if (Element == null) return; - _ = gestureManager.ChangeStateAsync(this, animated); + gestureManager.ChangeStateAsync(this, animated).SafeFireAndForget(); } internal void HandleLongPress() @@ -1114,7 +1199,7 @@ internal void HandleLongPress() void SetChildrenInputTransparent(bool value) { - if (!(Element is Layout layout)) + if (Element is not Layout layout) return; layout.ChildAdded -= OnLayoutChildAdded; @@ -1129,9 +1214,9 @@ void SetChildrenInputTransparent(bool value) layout.ChildAdded += OnLayoutChildAdded; } - void OnLayoutChildAdded(object sender, ElementEventArgs e) + void OnLayoutChildAdded(object? sender, ElementEventArgs e) { - if (!(e.Element is View view)) + if (e.Element is not View view) return; view.InputTransparent = ShouldMakeChildrenInputTransparent && diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/VisualFeedback/TouchEvents.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/VisualFeedback/TouchEvents.ios.cs index 0c85b3c21..7f4713499 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/VisualFeedback/TouchEvents.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/VisualFeedback/TouchEvents.ios.cs @@ -5,13 +5,13 @@ namespace Xamarin.CommunityToolkit.iOS.Effects [Foundation.Preserve(AllMembers = true)] class TouchEvents { - public event EventHandler TouchBegin; + public event EventHandler? TouchBegin; - public event EventHandler TouchMove; + public event EventHandler? TouchMove; - public event EventHandler TouchEnd; + public event EventHandler? TouchEnd; - public event EventHandler TouchCancel; + public event EventHandler? TouchCancel; public virtual void OnTouchBegin() => TouchBegin?.Invoke(this, EventArgs.Empty); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/VisualFeedback/VisualFeedbackEffectRouter.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/VisualFeedback/VisualFeedbackEffectRouter.android.cs index b77ee0b48..345fad8db 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/VisualFeedback/VisualFeedbackEffectRouter.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/VisualFeedback/VisualFeedbackEffectRouter.android.cs @@ -1,4 +1,5 @@ -using Android.Content.Res; +using System; +using Android.Content.Res; using Android.Graphics.Drawables; using Android.Runtime; using Android.Views; @@ -17,19 +18,19 @@ namespace Xamarin.CommunityToolkit.Android.Effects [Preserve(AllMembers = true)] public class VisualFeedbackEffectRouter : PlatformEffect { - AView view; - RippleDrawable ripple; - Drawable orgDrawable; - FrameLayout rippleOverlay; - FastRendererOnLayoutChangeListener fastListener; + AView? view; + RippleDrawable? ripple; + Drawable? orgDrawable; + FrameLayout? rippleOverlay; + FastRendererOnLayoutChangeListener? fastListener; - bool IsClickable => !(Element is Layout || Element is BoxView); + bool IsClickable => Element is not Layout or BoxView; protected override void OnAttached() { view = Control ?? Container; - SetUpRipple(); + SetUpRipple(view); if (IsClickable) view.Touch += OnViewTouch; @@ -41,18 +42,32 @@ protected override void OnDetached() { if (!IsClickable) { - view.Touch -= OnOverlayTouch; - view.RemoveOnLayoutChangeListener(fastListener); + if (view != null) + { + view.Touch -= OnOverlayTouch; + view.RemoveOnLayoutChangeListener(fastListener); + } + + if (fastListener != null) + { + fastListener.Dispose(); + fastListener = null; + } - fastListener.Dispose(); - fastListener = null; - rippleOverlay.Dispose(); - rippleOverlay = null; + if (rippleOverlay != null) + { + rippleOverlay.Dispose(); + rippleOverlay = null; + } } else { - view.Touch -= OnViewTouch; - view.Background = orgDrawable; + if (view != null) + { + view.Touch -= OnViewTouch; + view.Background = orgDrawable; + } + orgDrawable = null; } @@ -82,13 +97,13 @@ void UpdateEffectColor() ripple?.SetColor(GetPressedColorSelector(nativeColor)); } - void SetUpRipple() + void SetUpRipple(in AView view) { ripple = CreateRipple(AColor.Transparent); if (!IsClickable) { - rippleOverlay = new FrameLayout(view.Context) + rippleOverlay = new FrameLayout(view.Context ?? throw new NullReferenceException()) { Clickable = true, LongClickable = true, @@ -107,17 +122,18 @@ void SetUpRipple() void SetUpOverlay() { - var parent = view.Parent as ViewGroup; + var parent = view?.Parent as ViewGroup; - parent.AddView(rippleOverlay); + parent?.AddView(rippleOverlay); + _ = rippleOverlay ?? throw new NullReferenceException(); rippleOverlay.BringToFront(); rippleOverlay.Touch += OnOverlayTouch; } - void OnViewTouch(object sender, AView.TouchEventArgs e) => e.Handled = false; + void OnViewTouch(object? sender, AView.TouchEventArgs e) => e.Handled = false; - void OnOverlayTouch(object sender, AView.TouchEventArgs e) + void OnOverlayTouch(object? sender, AView.TouchEventArgs e) { view?.DispatchTouchEvent(e.Event); @@ -132,7 +148,7 @@ RippleDrawable CreateRipple(AColor color) return new RippleDrawable(GetPressedColorSelector(color), null, mask); } - var back = view.Background; + var back = view?.Background; if (back == null) { @@ -156,16 +172,14 @@ RippleDrawable CreateRipple(AColor color) internal class FastRendererOnLayoutChangeListener : Java.Lang.Object, AView.IOnLayoutChangeListener { bool hasParent = false; - VisualFeedbackEffectRouter effect; + VisualFeedbackEffectRouter? effect; - public FastRendererOnLayoutChangeListener(VisualFeedbackEffectRouter effect) - { - this.effect = effect; - } + public FastRendererOnLayoutChangeListener(VisualFeedbackEffectRouter effect) => this.effect = effect; - public void OnLayoutChange(AView v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) + public void OnLayoutChange(AView? v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) { - effect.rippleOverlay.Layout(v.Left, v.Top, v.Right, v.Bottom); + if (v != null) + effect?.rippleOverlay?.Layout(v.Left, v.Top, v.Right, v.Bottom); if (hasParent) { @@ -173,7 +187,7 @@ public void OnLayoutChange(AView v, int left, int top, int right, int bottom, in } hasParent = true; - effect.SetUpOverlay(); + effect?.SetUpOverlay(); } protected override void Dispose(bool disposing) diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/VisualFeedback/VisualFeedbackEffectRouter.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/VisualFeedback/VisualFeedbackEffectRouter.ios.cs index 910611543..c93ae532e 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/VisualFeedback/VisualFeedbackEffectRouter.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Effects/VisualFeedback/VisualFeedbackEffectRouter.ios.cs @@ -12,10 +12,10 @@ namespace Xamarin.CommunityToolkit.iOS.Effects [Foundation.Preserve(AllMembers = true)] public class VisualFeedbackEffectRouter : PlatformEffect { - TouchEvents touchEvents; - TouchEventsGestureRecognizer touchRecognizer; - UIView view; - UIView layer; + TouchEvents? touchEvents; + TouchEventsGestureRecognizer? touchRecognizer; + UIView? view; + UIView? layer; float alpha; protected override void OnAttached() @@ -57,22 +57,29 @@ protected override void OnAttached() protected override void OnDetached() { - touchEvents.TouchBegin -= OnTouchBegin; - touchEvents.TouchEnd -= OnTouchEnd; - touchEvents.TouchCancel -= OnTouchEnd; + if (touchEvents != null) + { + touchEvents.TouchBegin -= OnTouchBegin; + touchEvents.TouchEnd -= OnTouchEnd; + touchEvents.TouchCancel -= OnTouchEnd; + } - view.RemoveGestureRecognizer(touchRecognizer); - touchRecognizer.Delegate?.Dispose(); - touchRecognizer.Delegate = null; - touchRecognizer.Dispose(); + if (view != null && touchRecognizer != null) + { + view.RemoveGestureRecognizer(touchRecognizer); + touchRecognizer.Delegate.Dispose(); + touchRecognizer.Dispose(); + } - touchEvents = null; - touchRecognizer = null; + if (layer != null) + { + layer.RemoveFromSuperview(); + layer.Dispose(); + } - layer.RemoveFromSuperview(); - layer.Dispose(); layer = null; - + touchRecognizer = null; + touchEvents = null; view = null; } @@ -90,30 +97,34 @@ void UpdateEffectColor() { var color = VisualFeedbackEffect.GetFeedbackColor(Element); alpha = color.A < 1.0f ? 1f : 0.1f; - layer.BackgroundColor = color.ToUIColor(); + + if (layer != null) + layer.BackgroundColor = color.ToUIColor(); } - async void OnTouchBegin(object sender, EventArgs e) + async void OnTouchBegin(object? sender, EventArgs e) { - if (!(Element is VisualElement visualElement) || !visualElement.IsEnabled) + if (Element is not VisualElement visualElement || !visualElement.IsEnabled) return; - view.BecomeFirstResponder(); + view?.BecomeFirstResponder(); await UIView.AnimateAsync(0.5, () => { - layer.Alpha = alpha; + if (layer != null) + layer.Alpha = alpha; }); } - async void OnTouchEnd(object sender, EventArgs e) + async void OnTouchEnd(object? sender, EventArgs e) { - if (!(Element is VisualElement visualElement) || !visualElement.IsEnabled) + if (Element is not VisualElement visualElement || !visualElement.IsEnabled) return; await UIView.AnimateAsync(0.5, () => { - layer.Alpha = 0; + if (layer != null) + layer.Alpha = 0; }); } } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/ImageResourceExtension.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/ImageResourceExtension.shared.cs index 66090e203..b6042fe2d 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/ImageResourceExtension.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/ImageResourceExtension.shared.cs @@ -11,14 +11,14 @@ namespace Xamarin.CommunityToolkit.Extensions /// Provides ImageSource by Resource Id from the current app's assembly. /// [ContentProperty(nameof(Id))] - public class ImageResourceExtension : IMarkupExtension + public class ImageResourceExtension : IMarkupExtension { /// /// The Resource Id of the image. /// - public string Id { get; set; } + public string? Id { get; set; } - public ImageSource ProvideValue(IServiceProvider serviceProvider) + public ImageSource? ProvideValue(IServiceProvider serviceProvider) => Id == null ? null : ImageSource.FromResource(Id, Application.Current.GetType() @@ -27,7 +27,7 @@ public ImageSource ProvideValue(IServiceProvider serviceProvider) #endif .Assembly); - object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) - => ((IMarkupExtension)this).ProvideValue(serviceProvider); + object? IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) + => ((IMarkupExtension)this).ProvideValue(serviceProvider); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/JavaObjectExtensions.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/JavaObjectExtensions.android.cs new file mode 100644 index 000000000..e53f6c11e --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/JavaObjectExtensions.android.cs @@ -0,0 +1,19 @@ +using System; + +namespace Xamarin.CommunityToolkit.Extensions +{ + internal static class JavaObjectExtensions + { + public static bool IsDisposed(this Java.Lang.Object obj) + => obj.Handle == IntPtr.Zero; + + public static bool IsAlive(this Java.Lang.Object obj) + => obj != null && !obj.IsDisposed(); + + public static bool IsDisposed(this global::Android.Runtime.IJavaObject obj) + => obj.Handle == IntPtr.Zero; + + public static bool IsAlive(this global::Android.Runtime.IJavaObject obj) + => obj != null && !obj.IsDisposed(); + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/NavigableElement/NavigableElementExtensions.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/NavigableElement/NavigableElementExtensions.shared.cs new file mode 100644 index 000000000..77cebc2d7 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/NavigableElement/NavigableElementExtensions.shared.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using Xamarin.CommunityToolkit.UI.Views; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.Extensions +{ + /// + /// Extension methods for + /// + public static class NavigableElementExtensions + { + /// + /// Displays a popup. + /// + /// + /// The current that has a valid . + /// + /// + /// The to display. + /// + public static void ShowPopup(this NavigableElement element, BasePopup popup) => + element.Navigation.ShowPopup(popup); + + /// + /// Displays a poup and returns a result. + /// + /// + /// The result that is returned when the popup is dismissed. + /// + /// + /// The current that has a valid . + /// + /// + /// The to display. + /// + /// + /// A task that will complete once the is dismissed. + /// + public static Task ShowPopupAsync(this NavigableElement element, Popup popup) => + element.Navigation.ShowPopupAsync(popup); + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.android.cs new file mode 100644 index 000000000..164422241 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.android.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using Xamarin.CommunityToolkit.UI.Views; +using Xamarin.Forms.Platform.Android; + +namespace Xamarin.CommunityToolkit.Extensions +{ + public static partial class NavigationExtensions + { + static void PlatformShowPopup(BasePopup popup) => + Platform.CreateRendererWithContext(popup, ToolkitPlatform.Context); + + static Task PlatformShowPopupAsync(Popup popup) + { + PlatformShowPopup(popup); + return popup.Result; + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.ios.cs new file mode 100644 index 000000000..4c7eaf4a8 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.ios.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using Xamarin.CommunityToolkit.UI.Views; +using Xamarin.Forms.Platform.iOS; + +namespace Xamarin.CommunityToolkit.Extensions +{ + public static partial class NavigationExtensions + { + static void PlatformShowPopup(BasePopup popup) => + Platform.CreateRenderer(popup); + + static Task PlatformShowPopupAsync(Popup popup) + { + PlatformShowPopup(popup); + return popup.Result; + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.netstandard.macos.tvos.watchos.tizen.wpf.gtk.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.netstandard.macos.tvos.watchos.tizen.wpf.gtk.cs new file mode 100644 index 000000000..61a1c791b --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.netstandard.macos.tvos.watchos.tizen.wpf.gtk.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading.Tasks; +using Xamarin.CommunityToolkit.UI.Views; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.Extensions +{ + public static partial class NavigationExtensions + { + static void PlatformShowPopup(BasePopup popup) => + throw new NotSupportedException($"The current platform '{Device.RuntimePlatform}' does not support Xamarin.CommunityToolkit.BasePopup"); + + static Task PlatformShowPopupAsync(Popup popup) => + throw new NotSupportedException($"The current platform '{Device.RuntimePlatform}' does not support Xamarin.CommunityToolkit.Popup."); + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.shared.cs new file mode 100644 index 000000000..532bcd1b2 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.shared.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using Xamarin.CommunityToolkit.UI.Views; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.Extensions +{ + /// + /// Extension methods for . + /// + public static partial class NavigationExtensions + { + /// + /// Displays a popup. + /// + /// + /// The current . + /// + /// + /// The to display. + /// + public static void ShowPopup(this INavigation navigation, BasePopup popup) => + PlatformShowPopup(popup); + + /// + /// Displays a popup and returns a result. + /// + /// + /// The result that is returned when the popup is dismissed. + /// + /// + /// The current . + /// + /// + /// The to display. + /// + /// + /// A task that will complete once the is dismissed. + /// + public static Task ShowPopupAsync(this INavigation navigation, Popup popup) => + PlatformShowPopupAsync(popup); + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.uwp.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.uwp.cs new file mode 100644 index 000000000..642b10350 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/Navigation/NavigationExtensions.uwp.cs @@ -0,0 +1,42 @@ +using System.Linq; +using System.Threading.Tasks; +using Xamarin.CommunityToolkit.UI.Views; +using Xamarin.Forms; +using Xamarin.Forms.Platform.UWP; + +namespace Xamarin.CommunityToolkit.Extensions +{ + public static partial class NavigationExtensions + { + static void PlatformShowPopup(BasePopup popup) + { + popup.Parent = GetCurrentPage(Application.Current.MainPage); + Platform.CreateRenderer(popup); + + // https://github.com/xamarin/Xamarin.Forms/blob/0c95d0976cc089fe72476fb037851a64987de83c/Xamarin.Forms.Platform.iOS/PageExtensions.cs#L44 + Page GetCurrentPage(Page currentPage) + { + if (currentPage.NavigationProxy.ModalStack.LastOrDefault() is Page modal) + return modal; +#pragma warning disable CS0618 // Type or member is obsolete + else if (currentPage is MasterDetailPage mdp) +#pragma warning restore CS0618 // Type or member is obsolete + return GetCurrentPage(mdp.Detail); + else if (currentPage is FlyoutPage fp) + return GetCurrentPage(fp.Detail); + else if (currentPage is Shell shell && shell.CurrentItem?.CurrentItem is IShellSectionController ssc) + return ssc.PresentedPage; + else if (currentPage is IPageContainer pc) + return GetCurrentPage(pc.CurrentPage); + else + return currentPage; + } + } + + static Task PlatformShowPopupAsync(Popup popup) + { + PlatformShowPopup(popup); + return popup.Result; + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/TranslateExtension.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/TranslateExtension.shared.cs index 07068996f..b6d0a4393 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/TranslateExtension.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/TranslateExtension.shared.cs @@ -8,15 +8,17 @@ namespace Xamarin.CommunityToolkit.Extensions [ContentProperty(nameof(Text))] public class TranslateExtension : IMarkupExtension { - public string Text { get; set; } + public string Text { get; set; } = string.Empty; - public string StringFormat { get; set; } + public string? StringFormat { get; set; } object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) => ProvideValue(serviceProvider); public BindingBase ProvideValue(IServiceProvider serviceProvider) { -#if !NETSTANDARD1_0 +#if NETSTANDARD1_0 +throw new NotSupportedException("Translate XAML MarkupExtension is not supported on .NET Standard 1.0"); +#else #region Required work-around to prevent linker from removing the implementation if (DateTime.Now.Ticks < 0) _ = LocalizationResourceManager.Current[Text]; @@ -30,8 +32,6 @@ public BindingBase ProvideValue(IServiceProvider serviceProvider) StringFormat = StringFormat }; return binding; -#else - throw new NotSupportedException("Translate XAML MarkupExtension is not supported on .NET Standard 1.0"); #endif } } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/VisualElementExtension.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/VisualElement/VisualElementExtension.shared.cs similarity index 81% rename from src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/VisualElementExtension.shared.cs rename to src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/VisualElement/VisualElementExtension.shared.cs index faf626a5d..3d9896008 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/VisualElementExtension.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/VisualElement/VisualElementExtension.shared.cs @@ -4,13 +4,17 @@ namespace Xamarin.CommunityToolkit.Extensions { - public static class VisualElementExtension + /// + /// Extension methods for . + /// + public static partial class VisualElementExtension { - public static Task ColorTo(this VisualElement element, Color color, uint length = 250u, Easing easing = null) + public static Task ColorTo(this VisualElement element, Color color, uint length = 250u, Easing? easing = null) { _ = element ?? throw new ArgumentNullException(nameof(element)); var animationCompletionSource = new TaskCompletionSource(); + new Animation { { 0, 1, new Animation(v => element.BackgroundColor = new Color(v, element.BackgroundColor.G, element.BackgroundColor.B, element.BackgroundColor.A), element.BackgroundColor.R, color.R) }, @@ -18,6 +22,7 @@ public static Task ColorTo(this VisualElement element, Color color, uint l { 0, 1, new Animation(v => element.BackgroundColor = new Color(element.BackgroundColor.R, element.BackgroundColor.G, v, element.BackgroundColor.A), element.BackgroundColor.B, color.B) }, { 0, 1, new Animation(v => element.BackgroundColor = new Color(element.BackgroundColor.R, element.BackgroundColor.G, element.BackgroundColor.B, v), element.BackgroundColor.A, color.A) }, }.Commit(element, nameof(ColorTo), 16, length, easing, (d, b) => animationCompletionSource.SetResult(true)); + return animationCompletionSource.Task; } @@ -35,27 +40,29 @@ public static void AbortAnimations(this VisualElement element, params string[] o element.AbortAnimation(name); } - internal static bool TryFindParentElementWithParentOfType(this VisualElement element, out VisualElement result, out T parent) where T : VisualElement + internal static bool TryFindParentElementWithParentOfType(this VisualElement? element, out VisualElement? result, out T? parent) where T : VisualElement { - _ = element ?? throw new ArgumentNullException(nameof(element)); - result = null; parent = null; + while (element?.Parent != null) { - if (!(element.Parent is T parentElement)) + if (element.Parent is not T parentElement) { element = element.Parent as VisualElement; continue; } + result = element; parent = parentElement; + return true; } + return false; } - internal static bool TryFindParentOfType(this VisualElement element, out T parent) where T : VisualElement - => TryFindParentElementWithParentOfType(element, out _, out parent); + internal static bool TryFindParentOfType(this VisualElement? element, out T? parent) where T : VisualElement + => TryFindParentElementWithParentOfType(element, out _, out parent); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/VisualElement/VisualElementExtensions.uwp.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/VisualElement/VisualElementExtensions.uwp.cs new file mode 100644 index 000000000..53ddcc1b0 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Extensions/VisualElement/VisualElementExtensions.uwp.cs @@ -0,0 +1,51 @@ +using System; +using Xamarin.Forms; +using Xamarin.Forms.Platform.UWP; + +namespace Xamarin.CommunityToolkit.Extensions +{ + /// + /// Extension methods for . + /// + public static partial class VisualElementExtensions + { + + /// + /// cleanup object to dispose and + /// destroy resources. + /// + /// + /// The to cleanup. + /// + /// + /// This extension method is ported from Xamarin.Forms and should remain in sync. + /// + internal static void Cleanup(this VisualElement self) + { + if (self == null) + throw new ArgumentNullException("self"); + + var renderer = Platform.GetRenderer(self); + + foreach (var element in self.Descendants()) + { + var visual = element as VisualElement; + if (visual == null) + continue; + + var childRenderer = Platform.GetRenderer(visual); + if (childRenderer != null) + { + childRenderer.Dispose(); + Platform.SetRenderer(visual, null); + } + } + + if (renderer != null) + { + renderer.Dispose(); + Platform.SetRenderer(self, null); + } + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/DelegateWeakEventManager.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/DelegateWeakEventManager.shared.cs index 579ce86f8..1d9839227 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/DelegateWeakEventManager.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/DelegateWeakEventManager.shared.cs @@ -18,7 +18,7 @@ public class DelegateWeakEventManager /// /// Handler /// Event name - public void AddEventHandler(Delegate handler, [CallerMemberName] string eventName = "") + public void AddEventHandler(Delegate? handler, [CallerMemberName] string eventName = "") { if (IsNullOrWhiteSpace(eventName)) throw new ArgumentNullException(nameof(eventName)); @@ -26,7 +26,9 @@ public void AddEventHandler(Delegate handler, [CallerMemberName] string eventNam if (handler == null) throw new ArgumentNullException(nameof(handler)); - EventManagerService.AddEventHandler(eventName, handler.Target, handler.GetMethodInfo(), eventHandlers); + var methodInfo = handler.GetMethodInfo() ?? throw new NullReferenceException("Could not locate MethodInfo"); + + EventManagerService.AddEventHandler(eventName, handler.Target, methodInfo, eventHandlers); } /// @@ -34,7 +36,7 @@ public void AddEventHandler(Delegate handler, [CallerMemberName] string eventNam /// /// Handler /// Event name - public void RemoveEventHandler(Delegate handler, [CallerMemberName] string eventName = "") + public void RemoveEventHandler(Delegate? handler, [CallerMemberName] string eventName = "") { if (IsNullOrWhiteSpace(eventName)) throw new ArgumentNullException(nameof(eventName)); @@ -42,7 +44,9 @@ public void RemoveEventHandler(Delegate handler, [CallerMemberName] string event if (handler == null) throw new ArgumentNullException(nameof(handler)); - EventManagerService.RemoveEventHandler(eventName, handler.Target, handler.GetMethodInfo(), eventHandlers); + var methodInfo = handler.GetMethodInfo() ?? throw new NullReferenceException("Could not locate MethodInfo"); + + EventManagerService.RemoveEventHandler(eventName, handler.Target, methodInfo, eventHandlers); } /// @@ -51,7 +55,7 @@ public void RemoveEventHandler(Delegate handler, [CallerMemberName] string event /// Sender /// Event arguments /// Event name - public void HandleEvent(object sender, object eventArgs, string eventName) => RaiseEvent(sender, eventArgs, eventName); + public void HandleEvent(object? sender, object eventArgs, string eventName) => RaiseEvent(sender, eventArgs, eventName); /// /// Invokes the event Action @@ -65,7 +69,7 @@ public void RemoveEventHandler(Delegate handler, [CallerMemberName] string event /// Sender /// Event arguments /// Event name - public void RaiseEvent(object sender, object eventArgs, string eventName) => + public void RaiseEvent(object? sender, object eventArgs, string eventName) => EventManagerService.HandleEvent(eventName, sender, eventArgs, eventHandlers); /// diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/LocalizationResourceManager.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/LocalizationResourceManager.shared.cs index c2d9d4b24..df44e3635 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/LocalizationResourceManager.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/LocalizationResourceManager.shared.cs @@ -3,52 +3,55 @@ using System.Globalization; using System.Resources; using System.Threading; +using Xamarin.CommunityToolkit.ObjectModel; namespace Xamarin.CommunityToolkit.Helpers { #if !NETSTANDARD1_0 - public class LocalizationResourceManager : INotifyPropertyChanged + public class LocalizationResourceManager : ObservableObject { -#pragma warning disable CS0618 // Type or member is obsolete - public static LocalizationResourceManager Current { get; } = new LocalizationResourceManager(); -#pragma warning restore CS0618 // Type or member is obsolete + static readonly Lazy currentHolder = new Lazy(() => new LocalizationResourceManager()); - ResourceManager resourceManager; + public static LocalizationResourceManager Current => currentHolder.Value; + + ResourceManager? resourceManager; CultureInfo currentCulture = Thread.CurrentThread.CurrentUICulture; - [Obsolete("Please use the Current property instead of creating a new instance of this class")] - [EditorBrowsable(EditorBrowsableState.Never)] - public LocalizationResourceManager() + LocalizationResourceManager() { } - public void Init(ResourceManager resource) => - resourceManager = resource; + public void Init(ResourceManager resource) => resourceManager = resource; public void Init(ResourceManager resource, CultureInfo initialCulture) { - resourceManager = resource; - SetCulture(initialCulture); + CurrentCulture = initialCulture; + Init(resource); } - public string GetValue(string text) => - resourceManager.GetString(text, CurrentCulture); - - public string this[string text] => - GetValue(text); - - public void SetCulture(CultureInfo language) + public string GetValue(string text) { - currentCulture = language; - Invalidate(); + if (resourceManager == null) + throw new InvalidOperationException($"Must call {nameof(LocalizationResourceManager)}.{nameof(Init)} first"); + + return resourceManager.GetString(text, CurrentCulture) ?? throw new NullReferenceException($"{nameof(text)}: {text} not found"); } - public CultureInfo CurrentCulture => currentCulture; + public string this[string text] => GetValue(text); - public event PropertyChangedEventHandler PropertyChanged; + [Obsolete("Please, use " + nameof(CurrentCulture) + " to set culture")] + [EditorBrowsable(EditorBrowsableState.Never)] + public void SetCulture(CultureInfo language) => CurrentCulture = language; - public void Invalidate() => - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null)); + public CultureInfo CurrentCulture + { + get => currentCulture; + set => SetProperty(ref currentCulture, value, null); + } + + [Obsolete("This method is no longer needed with new implementation of " + nameof(LocalizationResourceManager) + ". Please, remove all references to it.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public void Invalidate() => OnPropertyChanged(null); } #endif } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/LocalizedString.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/LocalizedString.shared.cs new file mode 100644 index 000000000..785145aa4 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/LocalizedString.shared.cs @@ -0,0 +1,33 @@ +using System; +using Xamarin.CommunityToolkit.ObjectModel; +using Xamarin.Forms.Internals; + +namespace Xamarin.CommunityToolkit.Helpers +{ +#if !NETSTANDARD1_0 + public class LocalizedString : ObservableObject + { + readonly Func generator; + + public LocalizedString(Func generator) + : this(LocalizationResourceManager.Current, generator) + { + } + + public LocalizedString(LocalizationResourceManager localizationManager, Func generator) + { + this.generator = generator; + + // This instance will be unsubscribed and GCed if no one references it + // since LocalizationResourceManager uses WeekEventManger + localizationManager.PropertyChanged += (sender, e) => OnPropertyChanged(null); + } + + [Preserve(Conditional = true)] + public string Localized => generator(); + + [Preserve(Conditional = true)] + public static implicit operator LocalizedString(Func func) => new LocalizedString(func); + } +#endif +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/SafeArea.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/SafeArea.shared.cs index 71b1d0742..aea243329 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/SafeArea.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/SafeArea.shared.cs @@ -50,10 +50,10 @@ bool Equals(SafeArea other) Right == other.Right && Bottom == other.Bottom); - public override bool Equals(object obj) - => !ReferenceEquals(null, obj) && - obj is SafeArea safeArea && - Equals(safeArea); + public override bool Equals(object? obj) + => obj is not null + && obj is SafeArea safeArea + && Equals(safeArea); public override int GetHashCode() { diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/SafeFireAndForgetExtensions.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/SafeFireAndForgetExtensions.shared.cs new file mode 100644 index 000000000..2ad792125 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/SafeFireAndForgetExtensions.shared.cs @@ -0,0 +1,74 @@ +using System; +using System.Threading.Tasks; + +// Inspired by https://github.com/brminnick/AsyncAwaitBestPractices +namespace Xamarin.CommunityToolkit.Helpers +{ + /// + /// Extension methods for System.Threading.Tasks.Task and System.Threading.Tasks.ValueTask + /// + static class SafeFireAndForgetExtensions + { + /// + /// Safely execute the ValueTask without waiting for it to complete before moving to the next line of code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void": https://johnthiriet.com/removing-async-void/. + /// + /// ValueTask. + /// If an exception is thrown in the ValueTask, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true, continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false, continue on a different context; this will allow the Synchronization Context to continue on a different thread + internal static void SafeFireAndForget(this ValueTask task, in Action? onException = null, in bool continueOnCapturedContext = false) => + HandleSafeFireAndForget(task, continueOnCapturedContext, onException); + + /// + /// Safely execute the ValueTask without waiting for it to complete before moving to the next line of code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void": https://johnthiriet.com/removing-async-void/. + /// + /// ValueTask. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true, continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false, continue on a different context; this will allow the Synchronization Context to continue on a different thread + /// Exception type. If an exception is thrown of a different type, it will not be handled + internal static void SafeFireAndForget(this ValueTask task, in Action? onException = null, in bool continueOnCapturedContext = false) where TException : Exception => + HandleSafeFireAndForget(task, continueOnCapturedContext, onException); + + /// + /// Safely execute the Task without waiting for it to complete before moving to the next line of code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void": https://johnthiriet.com/removing-async-void/. + /// + /// Task. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true, continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false, continue on a different context; this will allow the Synchronization Context to continue on a different thread + internal static void SafeFireAndForget(this Task task, in Action? onException = null, in bool continueOnCapturedContext = false) => + HandleSafeFireAndForget(task, continueOnCapturedContext, onException); + + /// + /// Safely execute the Task without waiting for it to complete before moving to the next line of code; commonly known as "Fire And Forget". Inspired by John Thiriet's blog post, "Removing Async Void": https://johnthiriet.com/removing-async-void/. + /// + /// Task. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true, continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false, continue on a different context; this will allow the Synchronization Context to continue on a different thread + /// Exception type. If an exception is thrown of a different type, it will not be handled + internal static void SafeFireAndForget(this Task task, in Action? onException = null, in bool continueOnCapturedContext = false) where TException : Exception => + HandleSafeFireAndForget(task, continueOnCapturedContext, onException); + + static async void HandleSafeFireAndForget(ValueTask valueTask, bool continueOnCapturedContext, Action? onException) where TException : Exception + { + try + { + await valueTask.ConfigureAwait(continueOnCapturedContext); + } + catch (TException ex) when (onException != null) + { + onException(ex); + } + } + + static async void HandleSafeFireAndForget(Task task, bool continueOnCapturedContext, Action? onException) where TException : Exception + { + try + { + await task.ConfigureAwait(continueOnCapturedContext); + } + catch (TException ex) when (onException != null) + { + onException(ex); + } + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/Subscription.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/Subscription.shared.cs index 0d26a4cb3..ac55aa507 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/Subscription.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/Subscription.shared.cs @@ -6,11 +6,11 @@ namespace Xamarin.CommunityToolkit.Helpers { struct Subscription { - public WeakReference Subscriber { get; } + public WeakReference? Subscriber { get; } public MethodInfo Handler { get; } - public Subscription(WeakReference subscriber, MethodInfo handler) + public Subscription(WeakReference? subscriber, MethodInfo handler) { Subscriber = subscriber; Handler = handler ?? throw new ArgumentNullException(nameof(handler)); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/WeakEventManager.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/WeakEventManager.shared.cs index 080a576a7..35231dd16 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/WeakEventManager.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/WeakEventManager.shared.cs @@ -29,7 +29,9 @@ public void AddEventHandler(EventHandler handler, [CallerMemberName] if (handler == null) throw new ArgumentNullException(nameof(handler)); - EventManagerService.AddEventHandler(eventName, handler.Target, handler.GetMethodInfo(), eventHandlers); + var methodInfo = handler.GetMethodInfo() ?? throw new NullReferenceException("Could not locate MethodInfo"); + + EventManagerService.AddEventHandler(eventName, handler.Target, methodInfo, eventHandlers); } /// @@ -45,7 +47,9 @@ public void AddEventHandler(Action action, [CallerMemberName] string if (action == null) throw new ArgumentNullException(nameof(action)); - EventManagerService.AddEventHandler(eventName, action.Target, action.GetMethodInfo(), eventHandlers); + var methodInfo = action.GetMethodInfo() ?? throw new NullReferenceException("Could not locate MethodInfo"); + + EventManagerService.AddEventHandler(eventName, action.Target, methodInfo, eventHandlers); } /// @@ -61,7 +65,9 @@ public void RemoveEventHandler(EventHandler handler, [CallerMemberNa if (handler == null) throw new ArgumentNullException(nameof(handler)); - EventManagerService.RemoveEventHandler(eventName, handler.Target, handler.GetMethodInfo(), eventHandlers); + var methodInfo = handler.GetMethodInfo() ?? throw new NullReferenceException("Could not locate MethodInfo"); + + EventManagerService.RemoveEventHandler(eventName, handler.Target, methodInfo, eventHandlers); } /// @@ -77,7 +83,9 @@ public void RemoveEventHandler(Action action, [CallerMemberName] str if (action == null) throw new ArgumentNullException(nameof(action)); - EventManagerService.RemoveEventHandler(eventName, action.Target, action.GetMethodInfo(), eventHandlers); + var methodInfo = action.GetMethodInfo() ?? throw new NullReferenceException("Could not locate MethodInfo"); + + EventManagerService.RemoveEventHandler(eventName, action.Target, methodInfo, eventHandlers); } /// @@ -86,7 +94,7 @@ public void RemoveEventHandler(Action action, [CallerMemberName] str /// Sender /// Event arguments /// Event name - public void HandleEvent(object sender, TEventArgs eventArgs, string eventName) => RaiseEvent(sender, eventArgs, eventName); + public void HandleEvent(object? sender, TEventArgs eventArgs, string eventName) => RaiseEvent(sender, eventArgs, eventName); /// /// Invokes the event Action @@ -101,7 +109,7 @@ public void RemoveEventHandler(Action action, [CallerMemberName] str /// Sender /// Event arguments /// Event name - public void RaiseEvent(object sender, TEventArgs eventArgs, string eventName) => + public void RaiseEvent(object? sender, TEventArgs eventArgs, string eventName) => EventManagerService.HandleEvent(eventName, sender, eventArgs, eventHandlers); /// @@ -113,15 +121,19 @@ public void RemoveEventHandler(Action action, [CallerMemberName] str EventManagerService.HandleEvent(eventName, eventArgs, eventHandlers); } + /// + /// Extensions for Xamarin.Forms.WeakEventManager + /// public static class WeakEventManagerExtensions { /// /// Invokes the event EventHandler /// + /// WeakEventManager /// Sender /// Event arguments /// Event name - public static void RaiseEvent(this Forms.WeakEventManager weakEventManager, object sender, object eventArgs, string eventName) + public static void RaiseEvent(this Forms.WeakEventManager weakEventManager, object? sender, object eventArgs, string eventName) { _ = weakEventManager ?? throw new ArgumentNullException(nameof(weakEventManager)); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/WeakEventManagerService.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/WeakEventManagerService.shared.cs index 4c78694dd..3bf915edf 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/WeakEventManagerService.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Helpers/WeakEventManagerService.shared.cs @@ -10,7 +10,7 @@ namespace Xamarin.CommunityToolkit.Helpers { static class EventManagerService { - internal static void AddEventHandler(in string eventName, in object handlerTarget, in MethodInfo methodInfo, in Dictionary> eventHandlers) + internal static void AddEventHandler(in string eventName, in object? handlerTarget, in MethodInfo methodInfo, in Dictionary> eventHandlers) { var doesContainSubscriptions = eventHandlers.TryGetValue(eventName, out var targets); if (!doesContainSubscriptions || targets == null) @@ -25,7 +25,7 @@ internal static void AddEventHandler(in string eventName, in object handlerTarge targets.Add(new Subscription(new WeakReference(handlerTarget), methodInfo)); } - internal static void RemoveEventHandler(in string eventName, in object handlerTarget, in MemberInfo methodInfo, in Dictionary> eventHandlers) + internal static void RemoveEventHandler(in string eventName, in object? handlerTarget, in MemberInfo methodInfo, in Dictionary> eventHandlers) { var doesContainSubscriptions = eventHandlers.TryGetValue(eventName, out var subscriptions); if (!doesContainSubscriptions || subscriptions == null) @@ -46,7 +46,7 @@ internal static void RemoveEventHandler(in string eventName, in object handlerTa } } - internal static void HandleEvent(in string eventName, in object sender, in object eventArgs, in Dictionary> eventHandlers) + internal static void HandleEvent(in string eventName, in object? sender, in object? eventArgs, in Dictionary> eventHandlers) { AddRemoveEvents(eventName, eventHandlers, out var toRaise); @@ -72,7 +72,7 @@ internal static void HandleEvent(in string eventName, in object sender, in objec } } - internal static void HandleEvent(in string eventName, in object actionEventArgs, in Dictionary> eventHandlers) + internal static void HandleEvent(in string eventName, in object? actionEventArgs, in Dictionary> eventHandlers) { AddRemoveEvents(eventName, eventHandlers, out var toRaise); @@ -93,7 +93,7 @@ internal static void HandleEvent(in string eventName, in object actionEventArgs, } catch (TargetParameterCountException e) { - throw new InvalidHandleEventException("Parameter count mismatch. If invoking an `event EventHandler` use `HandleEvent(object sender, TEventArgs eventArgs, string eventName)` or if invoking an `event Action` use `HandleEvent(string eventName)`instead.", e); + throw new InvalidHandleEventException("Parameter count mismatch. If invoking an `event EventHandler` use `HandleEvent(object? sender, TEventArgs eventArgs, string eventName)` or if invoking an `event Action` use `HandleEvent(string eventName)`instead.", e); } } } @@ -119,15 +119,15 @@ internal static void HandleEvent(in string eventName, in Dictionary` use `HandleEvent(object eventArgs, string eventName)`instead.", e); + throw new InvalidHandleEventException("Parameter count mismatch. If invoking an `event EventHandler` use `HandleEvent(object? sender, TEventArgs eventArgs, string eventName)` or if invoking an `event Action` use `HandleEvent(object eventArgs, string eventName)`instead.", e); } } } - static void AddRemoveEvents(in string eventName, in Dictionary> eventHandlers, out List<(object Instance, MethodInfo EventHandler)> toRaise) + static void AddRemoveEvents(in string eventName, in Dictionary> eventHandlers, out List<(object? Instance, MethodInfo EventHandler)> toRaise) { var toRemove = new List(); - toRaise = new List<(object, MethodInfo)>(); + toRaise = new List<(object?, MethodInfo)>(); var doesContainEventName = eventHandlers.TryGetValue(eventName, out var target); if (doesContainEventName && target != null) @@ -159,20 +159,19 @@ static void AddRemoveEvents(in string eventName, in Dictionary f.Name is "m_owner").GetValue(rtDynamicMethod) - : null; + if (typeInfoRTDynamicMethod != null && typeInfoRTDynamicMethod.IsAssignableFrom(rtDynamicMethod.GetType().GetTypeInfo())) + return (DynamicMethod?)typeRTDynamicMethod?.GetRuntimeFields()?.FirstOrDefault(f => f?.Name is "m_owner")?.GetValue(rtDynamicMethod); + else + return null; } static bool IsLightweightMethod(this MethodBase method) { - _ = method ?? throw new ArgumentNullException(nameof(method)); - var typeInfoRTDynamicMethod = typeof(DynamicMethod).GetTypeInfo().GetDeclaredNestedType("RTDynamicMethod"); return method is DynamicMethod || (typeInfoRTDynamicMethod?.IsAssignableFrom(method.GetType().GetTypeInfo()) ?? false); } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/AsyncCommand.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/AsyncCommand.shared.cs index 341f711aa..1a576a477 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/AsyncCommand.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/AsyncCommand.shared.cs @@ -18,9 +18,9 @@ public class AsyncCommand : BaseAsyncCommandIf an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread public AsyncCommand( - Func execute, - Func canExecute = null, - Action onException = null, + Func execute, + Func? canExecute = null, + Action? onException = null, bool continueOnCapturedContext = false, bool allowsMultipleExecutions = true) : base(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions) @@ -47,9 +47,9 @@ public class AsyncCommand : BaseAsyncCommand, IAsyncCommand /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread public AsyncCommand( - Func execute, - Func canExecute = null, - Action onException = null, + Func execute, + Func? canExecute = null, + Action? onException = null, bool continueOnCapturedContext = false, bool allowsMultipleExecutions = true) : base(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions) @@ -64,9 +64,9 @@ public class AsyncCommand : BaseAsyncCommand, IAsyncCommand /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread public AsyncCommand( - Func execute, + Func execute, Func canExecute, - Action onException = null, + Action? onException = null, bool continueOnCapturedContext = false, bool allowsMultipleExecutions = true) : this(execute, ConvertCanExecute(canExecute), onException, continueOnCapturedContext, allowsMultipleExecutions) @@ -94,8 +94,8 @@ public class AsyncCommand : BaseAsyncCommand, IAsyncCommand /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread public AsyncCommand( Func execute, - Func canExecute = null, - Action onException = null, + Func? canExecute = null, + Action? onException = null, bool continueOnCapturedContext = false, bool allowsMultipleExecutions = true) : base(ConvertExecute(execute), canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions) @@ -112,7 +112,7 @@ public class AsyncCommand : BaseAsyncCommand, IAsyncCommand public AsyncCommand( Func execute, Func canExecute, - Action onException = null, + Action? onException = null, bool continueOnCapturedContext = false, bool allowsMultipleExecutions = true) : this(execute, ConvertCanExecute(canExecute), onException, continueOnCapturedContext, allowsMultipleExecutions) diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/AsyncValueCommand.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/AsyncValueCommand.shared.cs index 5961f7a0a..60d5b4e7b 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/AsyncValueCommand.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/AsyncValueCommand.shared.cs @@ -15,9 +15,9 @@ public class AsyncValueCommand : BaseAsyncValueCommandIf an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread public AsyncValueCommand( - Func execute, - Func canExecute = null, - Action onException = null, + Func execute, + Func? canExecute = null, + Action? onException = null, bool continueOnCapturedContext = false, bool allowsMultipleExecutions = true) : base(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions) @@ -44,9 +44,9 @@ public class AsyncValueCommand : BaseAsyncValueCommand, IAsyncValu /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread public AsyncValueCommand( - Func execute, - Func canExecute = null, - Action onException = null, + Func execute, + Func? canExecute = null, + Action? onException = null, bool continueOnCapturedContext = false, bool allowsMultipleExecutions = true) : base(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions) @@ -61,9 +61,9 @@ public class AsyncValueCommand : BaseAsyncValueCommand, IAsyncValu /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread public AsyncValueCommand( - Func execute, + Func execute, Func canExecute, - Action onException = null, + Action? onException = null, bool continueOnCapturedContext = false, bool allowsMultipleExecutions = true) : this(execute, ConvertCanExecute(canExecute), onException, continueOnCapturedContext, allowsMultipleExecutions) @@ -91,8 +91,8 @@ public class AsyncValueCommand : BaseAsyncValueCommand, IAsyncVa /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread public AsyncValueCommand( Func execute, - Func canExecute = null, - Action onException = null, + Func? canExecute = null, + Action? onException = null, bool continueOnCapturedContext = false, bool allowsMultipleExecutions = true) : base(ConvertExecute(execute), canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions) @@ -109,7 +109,7 @@ public class AsyncValueCommand : BaseAsyncValueCommand, IAsyncVa public AsyncValueCommand( Func execute, Func canExecute, - Action onException = null, + Action? onException = null, bool continueOnCapturedContext = false, bool allowsMultipleExecutions = true) : this(execute, ConvertCanExecute(canExecute), onException, continueOnCapturedContext, allowsMultipleExecutions) diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/CommandFactory.Command.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/CommandFactory.Command.shared.cs new file mode 100644 index 000000000..b0b9ab702 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/CommandFactory.Command.shared.cs @@ -0,0 +1,59 @@ +using System; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.ObjectModel +{ + /// + /// Factory for Xamarin.Forms.Command + /// + public static partial class CommandFactory + { + /// + /// Initializes Xamarin.Forms.Command + /// + /// The Function executed when Execute is called. This does not check canExecute before executing and will execute even if canExecute is false + /// Xamarin.Forms.Command + public static Command Create(Action execute) => + new Command(execute); + + /// + /// Initializes Xamarin.Forms.Command + /// + /// The Function executed when Execute is called. This does not check canExecute before executing and will execute even if canExecute is false + /// Xamarin.Forms.Command + public static Command Create(Action execute, Func canExecute) => + new Command(execute, canExecute); + + /// + /// Initializes Xamarin.Forms.Command + /// + /// The Function executed when Execute is called. This does not check canExecute before executing and will execute even if canExecute is false + /// Xamarin.Forms.Command + public static Command Create(Action execute) => + new Command(execute); + + /// + /// Initializes Xamarin.Forms.Command + /// + /// The Function executed when Execute is called. This does not check canExecute before executing and will execute even if canExecute is false + /// Xamarin.Forms.Command + public static Command Create(Action execute, Func canExecute) => + new Command(execute, canExecute); + + /// + /// Initializes Xamarin.Forms.Command + /// + /// The Function executed when Execute is called. This does not check canExecute before executing and will execute even if canExecute is false + /// Xamarin.Forms.Command + public static Command Create(Action execute) => + new Command(execute); + + /// + /// Initializes Xamarin.Forms.Command + /// + /// The Function executed when Execute is called. This does not check canExecute before executing and will execute even if canExecute is false + /// Xamarin.Forms.Command + public static Command Create(Action execute, Func canExecute) => + new Command(execute, canExecute); + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/CommandFactory.IAsyncCommand.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/CommandFactory.IAsyncCommand.shared.cs new file mode 100644 index 000000000..effe09fc3 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/CommandFactory.IAsyncCommand.shared.cs @@ -0,0 +1,161 @@ +using System; +using System.Threading.Tasks; + +namespace Xamarin.CommunityToolkit.ObjectModel +{ + /// + /// Factory for IAsyncCommand + /// + public static partial class CommandFactory + { + /// + /// Initializes a new instance of IAsyncCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread + /// IAsyncCommand + public static IAsyncCommand Create( + Func execute, + Func? canExecute = null, + Action? onException = null, + bool continueOnCapturedContext = false, + bool allowsMultipleExecutions = true) => + new AsyncCommand(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions); + + /// + /// Initializes a new instance of IAsyncCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread + /// IAsyncCommand + public static IAsyncCommand Create( + Func execute, + Func canExecute, + Action? onException = null, + bool continueOnCapturedContext = false, + bool allowsMultipleExecutions = true) => + new AsyncCommand(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions); + + /// + /// Initializes a new instance of IAsyncCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread + /// IAsyncCommand + public static IAsyncCommand Create( + Func execute, + Func? canExecute = null, + Action? onException = null, + bool continueOnCapturedContext = false, + bool allowsMultipleExecutions = true) => + new AsyncCommand(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions); + + /// + /// Initializes a new instance of IAsyncCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread + /// IAsyncCommand + public static IAsyncCommand Create( + Func execute, + Func canExecute, + Action? onException = null, + bool continueOnCapturedContext = false, + bool allowsMultipleExecutions = true) => + new AsyncCommand(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions); + + /// + /// Initializes a new instance of IAsyncCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread + /// IAsyncCommand + public static IAsyncCommand Create( + Func execute, + Func? canExecute = null, + Action? onException = null, + bool continueOnCapturedContext = false, + bool allowsMultipleExecutions = true) => + new AsyncCommand(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions); + + #region Helper Methods to Prevent Ambiguous Method Error When Using Anonymous Async Methods + + /// + /// Initializes a new instance of IAsyncCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// IAsyncCommand + public static IAsyncCommand Create(Func execute) + { + Func? canExecute = null; + return Create(execute, canExecute, null, false, true); + } + + /// + /// Initializes a new instance of IAsyncCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// IAsyncCommand + public static IAsyncCommand Create(Func execute, Func canExecute) => + Create(execute, canExecute, null, false, true); + + /// + /// Initializes a new instance of IAsyncCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// IAsyncCommand + public static IAsyncCommand Create(Func execute, Func canExecute) => + Create(execute, canExecute, null, false, true); + + /// + /// Initializes a new instance of IAsyncCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// IAsyncCommand + public static IAsyncCommand Create(Func execute) + { + Func? canExecute = null; + return Create(execute, canExecute, null, false, true); + } + + /// + /// Initializes a new instance of IAsyncCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// IAsyncCommand + public static IAsyncCommand Create(Func execute, Func canExecute) => + Create(execute, canExecute, null, false, true); + + /// + /// Initializes a new instance of IAsyncCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// IAsyncCommand + public static IAsyncCommand Create(Func execute, Func canExecute) => + Create(execute, canExecute, null, false, true); + + /// + /// Initializes a new instance of IAsyncCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// IAsyncCommand + public static IAsyncCommand Create(Func execute, Func canExecute) => + Create(execute, canExecute, null, false, true); + #endregion + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/CommandFactory.IAsyncValueCommand.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/CommandFactory.IAsyncValueCommand.shared.cs new file mode 100644 index 000000000..df0aac91a --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/CommandFactory.IAsyncValueCommand.shared.cs @@ -0,0 +1,91 @@ +using System; +using System.Threading.Tasks; + +namespace Xamarin.CommunityToolkit.ObjectModel +{ + /// + /// Factory for IAsyncValueCommand + /// + public static partial class CommandFactory + { + /// + /// Initializes a new instance of AsyncValueCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread + /// IAsyncValueCommand + public static IAsyncValueCommand Create( + Func execute, + Func? canExecute = null, + Action? onException = null, + bool continueOnCapturedContext = false, + bool allowsMultipleExecutions = true) => + new AsyncValueCommand(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions); + + /// + /// Initializes a new instance of AsyncValueCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread + /// IAsyncValueCommand + public static IAsyncValueCommand Create( + Func execute, + Func canExecute, + Action? onException = null, + bool continueOnCapturedContext = false, + bool allowsMultipleExecutions = true) => + new AsyncValueCommand(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions); + + /// + /// Initializes a new instance of AsyncValueCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread + /// IAsyncValueCommand + public static IAsyncValueCommand Create( + Func execute, + Func? canExecute = null, + Action? onException = null, + bool continueOnCapturedContext = false, + bool allowsMultipleExecutions = true) => + new AsyncValueCommand(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions); + + /// + /// Initializes a new instance of AsyncValueCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread + /// IAsyncValueCommand + public static IAsyncValueCommand Create( + Func execute, + Func canExecute, + Action? onException = null, + bool continueOnCapturedContext = false, + bool allowsMultipleExecutions = true) => + new AsyncValueCommand(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions); + + /// + /// Initializes a new instance of AsyncValueCommand + /// + /// The Function executed when Execute or ExecuteAsync is called. This does not check canExecute before executing and will execute even if canExecute is false + /// The Function that verifies whether or not AsyncCommand should execute. + /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown + /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread + /// IAsyncValueCommand> + public static IAsyncValueCommand Create( + Func execute, + Func? canExecute = null, + Action? onException = null, + bool continueOnCapturedContext = false, + bool allowsMultipleExecutions = true) => + new AsyncValueCommand(execute, canExecute, onException, continueOnCapturedContext, allowsMultipleExecutions); + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Extensions/INotifyPropertyChangedExtension.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Extensions/INotifyPropertyChangedExtension.shared.cs new file mode 100644 index 000000000..97e79d2ed --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Extensions/INotifyPropertyChangedExtension.shared.cs @@ -0,0 +1,32 @@ +using System; +using System.ComponentModel; + +namespace Xamarin.CommunityToolkit.ObjectModel.Extensions +{ + public static class INotifyPropertyChangedExtension + { + public static void WeakSubscribe(this INotifyPropertyChanged target, T subscriber, Action action) + { + _ = target ?? throw new ArgumentNullException(nameof(target)); + if (subscriber == null || action == null) + { + return; + } + + var weakSubscriber = new WeakReference(subscriber, false); + target.PropertyChanged += handler; + + void handler(object? sender, PropertyChangedEventArgs e) + { + var s = (T?)weakSubscriber.Target; + if (s == null) + { + target.PropertyChanged -= handler; + return; + } + + action(s, sender, e); + } + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Extensions/ObservableRangeCollectionExtension.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Extensions/ObservableRangeCollectionExtension.shared.cs new file mode 100644 index 000000000..6385ec22b --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Extensions/ObservableRangeCollectionExtension.shared.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace Xamarin.CommunityToolkit.ObjectModel +{ + public static class ObservableRangeCollectionExtension + { + /// + /// To be used in collection initializer + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static void Add(this ObservableRangeCollection source, IEnumerable collection) + { + _ = source ?? throw new ArgumentNullException(nameof(source)); + + source.AddRange(collection); + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Grouping.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Grouping.shared.cs new file mode 100644 index 000000000..2db0d0e75 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Grouping.shared.cs @@ -0,0 +1,69 @@ +using System.Collections.Generic; + +#nullable enable + +namespace Xamarin.CommunityToolkit.ObjectModel +{ + /// + /// Grouping of items by key into ObservableRange + /// + public class Grouping : ObservableRangeCollection + { + /// + /// Gets the key. + /// + /// The key. + public TKey Key { get; } + + /// + /// Returns list of items in the grouping. + /// + public new IList Items => base.Items; + + /// + /// Initializes a new instance of the Grouping class. + /// + /// Key. + /// Items. + public Grouping(TKey key, IEnumerable items) + { + Key = key; + AddRange(items); + } + } + + /// + /// Grouping of items by key and subkey into ObservableRange + /// + public class Grouping : ObservableRangeCollection + { + /// + /// Gets the key. + /// + /// The key. + public TKey Key { get; } + + /// + /// Gets the subkey of the grouping + /// + public TSubKey SubKey { get; } + + /// + /// Returns list of items in the grouping. + /// + public new IList Items => base.Items; + + /// + /// Initializes a new instance of the Grouping class. + /// + /// Key. + /// Subkey + /// Items. + public Grouping(TKey key, TSubKey subkey, IEnumerable items) + { + Key = key; + SubKey = subkey; + AddRange(items); + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseAsyncCommand.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseAsyncCommand.shared.cs index eb6f8dbf8..1336600d3 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseAsyncCommand.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseAsyncCommand.shared.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using System.Windows.Input; using Xamarin.CommunityToolkit.Exceptions; +using Xamarin.CommunityToolkit.Helpers; namespace Xamarin.CommunityToolkit.ObjectModel.Internals { @@ -13,8 +14,8 @@ namespace Xamarin.CommunityToolkit.ObjectModel.Internals [EditorBrowsable(EditorBrowsableState.Never)] public class BaseAsyncCommand : BaseCommand, ICommand { - readonly Func execute; - readonly Action onException; + readonly Func execute; + readonly Action? onException; readonly bool continueOnCapturedContext; /// @@ -24,10 +25,10 @@ public class BaseAsyncCommand : BaseCommand, /// The Function that verifies whether or not AsyncCommand should execute. /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread - public BaseAsyncCommand( - Func execute, - Func canExecute, - Action onException, + protected private BaseAsyncCommand( + Func? execute, + Func? canExecute, + Action? onException, bool continueOnCapturedContext, bool allowsMultipleExecutions) : base(canExecute, allowsMultipleExecutions) @@ -42,7 +43,7 @@ public class BaseAsyncCommand : BaseCommand, /// /// /// The Execute parameter required for ICommand - private protected static Func ConvertExecute(Func execute) + private protected static Func? ConvertExecute(Func? execute) { if (execute == null) return null; @@ -55,7 +56,7 @@ public class BaseAsyncCommand : BaseCommand, /// /// /// The CanExecute parameter required for ICommand - private protected static Func ConvertCanExecute(Func canExecute) + private protected static Func? ConvertCanExecute(Func? canExecute) { if (canExecute == null) return null; @@ -68,7 +69,7 @@ public class BaseAsyncCommand : BaseCommand, /// /// The executed Task /// Data used by the command. If the command does not require data to be passed, this object can be set to null. - private protected async Task ExecuteAsync(TExecute parameter) + private protected async Task ExecuteAsync(TExecute? parameter) { ExecutionCount++; @@ -90,7 +91,7 @@ private protected async Task ExecuteAsync(TExecute parameter) bool ICommand.CanExecute(object parameter) => parameter switch { TCanExecute validParameter => CanExecute(validParameter), - null when !typeof(TCanExecute).GetTypeInfo().IsValueType => CanExecute((TCanExecute)parameter), + null when !typeof(TCanExecute).GetTypeInfo().IsValueType => CanExecute((TCanExecute?)parameter), null => throw new InvalidCommandParameterException(typeof(TCanExecute)), _ => throw new InvalidCommandParameterException(typeof(TCanExecute), parameter.GetType()), }; @@ -100,11 +101,11 @@ void ICommand.Execute(object parameter) switch (parameter) { case TExecute validParameter: - Execute(validParameter); + ExecuteAsync(validParameter).SafeFireAndForget(onException, continueOnCapturedContext); break; case null when !typeof(TExecute).GetTypeInfo().IsValueType: - Execute((TExecute)parameter); + ExecuteAsync((TExecute?)parameter).SafeFireAndForget(onException, continueOnCapturedContext); break; case null: @@ -113,9 +114,6 @@ void ICommand.Execute(object parameter) default: throw new InvalidCommandParameterException(typeof(TExecute), parameter.GetType()); } - - // Use local method to defer async void from ICommand.Execute, allowing InvalidCommandParameterException to be thrown on the calling thread context before reaching an async method - async void Execute(TExecute parameter) => await ExecuteAsync(parameter).ConfigureAwait(continueOnCapturedContext); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseAsyncValueCommand.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseAsyncValueCommand.shared.cs index c32ae7000..2f6edbc9a 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseAsyncValueCommand.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseAsyncValueCommand.shared.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using System.Windows.Input; using Xamarin.CommunityToolkit.Exceptions; +using Xamarin.CommunityToolkit.Helpers; namespace Xamarin.CommunityToolkit.ObjectModel.Internals { @@ -13,8 +14,8 @@ namespace Xamarin.CommunityToolkit.ObjectModel.Internals [EditorBrowsable(EditorBrowsableState.Never)] public abstract class BaseAsyncValueCommand : BaseCommand, ICommand { - readonly Func execute; - readonly Action onException; + readonly Func execute; + readonly Action? onException; readonly bool continueOnCapturedContext; /// @@ -24,10 +25,10 @@ public abstract class BaseAsyncValueCommand : BaseCommand /// The Function that verifies whether or not AsyncCommand should execute. /// If an exception is thrown in the Task, onException will execute. If onException is null, the exception will be re-thrown /// If set to true continue on captured context; this will ensure that the Synchronization Context returns to the calling thread. If set to false continue on a different context; this will allow the Synchronization Context to continue on a different thread - public BaseAsyncValueCommand( - Func execute, - Func canExecute, - Action onException, + protected private BaseAsyncValueCommand( + Func? execute, + Func? canExecute, + Action? onException, bool continueOnCapturedContext, bool allowsMultipleExecutions) : base(canExecute, allowsMultipleExecutions) @@ -42,7 +43,7 @@ public abstract class BaseAsyncValueCommand : BaseCommand /// /// /// The Execute parameter required for ICommand - private protected static Func ConvertExecute(Func execute) + private protected static Func? ConvertExecute(Func? execute) { if (execute == null) return null; @@ -55,7 +56,7 @@ public abstract class BaseAsyncValueCommand : BaseCommand /// /// /// The CanExecute parameter required for ICommand - private protected static Func ConvertCanExecute(Func canExecute) + private protected static Func? ConvertCanExecute(Func? canExecute) { if (canExecute == null) return null; @@ -68,7 +69,7 @@ public abstract class BaseAsyncValueCommand : BaseCommand /// /// The executed Value /// Data used by the command. If the command does not require data to be passed, this object can be set to null. - private protected async ValueTask ExecuteAsync(TExecute parameter) + private protected async ValueTask ExecuteAsync(TExecute? parameter) { ExecutionCount++; @@ -90,7 +91,7 @@ private protected async ValueTask ExecuteAsync(TExecute parameter) bool ICommand.CanExecute(object parameter) => parameter switch { TCanExecute validParameter => CanExecute(validParameter), - null when !typeof(TCanExecute).GetTypeInfo().IsValueType => CanExecute((TCanExecute)parameter), + null when !typeof(TCanExecute).GetTypeInfo().IsValueType => CanExecute((TCanExecute?)parameter), null => throw new InvalidCommandParameterException(typeof(TCanExecute)), _ => throw new InvalidCommandParameterException(typeof(TCanExecute), parameter.GetType()), }; @@ -100,11 +101,11 @@ void ICommand.Execute(object parameter) switch (parameter) { case TExecute validParameter: - Execute(validParameter); + ExecuteAsync(validParameter).SafeFireAndForget(onException, continueOnCapturedContext); break; case null when !typeof(TExecute).GetTypeInfo().IsValueType: - Execute((TExecute)parameter); + ExecuteAsync((TExecute?)parameter).SafeFireAndForget(onException, continueOnCapturedContext); break; case null: @@ -113,9 +114,6 @@ void ICommand.Execute(object parameter) default: throw new InvalidCommandParameterException(typeof(TExecute), parameter.GetType()); } - - // Use local method to defer async void from ICommand.Execute, allowing InvalidCommandParameterException to be thrown on the calling thread context before reaching an async method - async void Execute(TExecute parameter) => await ExecuteAsync(parameter).ConfigureAwait(continueOnCapturedContext); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseCommand.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseCommand.android.cs index 77fde7ebc..8815a0147 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseCommand.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseCommand.android.cs @@ -6,14 +6,14 @@ namespace Xamarin.CommunityToolkit.ObjectModel.Internals { public abstract partial class BaseCommand { - static volatile Handler handler; + static volatile Handler? handler; static bool IsMainThread { get { if (Build.VERSION.SdkInt >= BuildVersionCodes.M) - return Looper.MainLooper.IsCurrentThread; + return Looper.MainLooper?.IsCurrentThread ?? false; return Looper.MyLooper() == Looper.MainLooper; } @@ -21,8 +21,10 @@ static bool IsMainThread static void BeginInvokeOnMainThread(Action action) { - if (handler?.Looper != Looper.MainLooper) - handler = new Handler(Looper.MainLooper); + if (handler == null || handler.Looper != Looper.MainLooper) + { + handler = new Handler(Looper.MainLooper ?? throw new NullReferenceException($"{nameof(Looper.MainLooper)} cannot be null")); + } handler.Post(action); } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseCommand.gtk.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseCommand.gtk.cs index 37ef803e1..264893d13 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseCommand.gtk.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseCommand.gtk.cs @@ -5,9 +5,9 @@ namespace Xamarin.CommunityToolkit.ObjectModel.Internals { public abstract partial class BaseCommand { - static readonly Thread mainThread = Thread.CurrentThread; + readonly SynchronizationContext? synchronizationContext = SynchronizationContext.Current; - static bool IsMainThread => Thread.CurrentThread == mainThread; + bool IsMainThread => SynchronizationContext.Current == synchronizationContext; static void BeginInvokeOnMainThread(Action action) { diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseCommand.netstandard.wpf.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseCommand.netstandard.wpf.cs index 40d1ac7ce..ce1eb4a0b 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseCommand.netstandard.wpf.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseCommand.netstandard.wpf.cs @@ -6,11 +6,11 @@ namespace Xamarin.CommunityToolkit.ObjectModel.Internals { public abstract partial class BaseCommand { - static readonly SynchronizationContext synchronizationContext = SynchronizationContext.Current; + readonly SynchronizationContext? synchronizationContext = SynchronizationContext.Current; - static bool IsMainThread => SynchronizationContext.Current == synchronizationContext; + bool IsMainThread => SynchronizationContext.Current == synchronizationContext; - static void BeginInvokeOnMainThread(Action action) + void BeginInvokeOnMainThread(Action action) { if (synchronizationContext != null && SynchronizationContext.Current != synchronizationContext) synchronizationContext.Post(_ => action(), null); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseCommand.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseCommand.shared.cs index 1f094d662..252e6d958 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseCommand.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/Internals/BaseCommand.shared.cs @@ -11,17 +11,17 @@ namespace Xamarin.CommunityToolkit.ObjectModel.Internals [EditorBrowsable(EditorBrowsableState.Never)] public abstract partial class BaseCommand { - readonly Func canExecute; - readonly WeakEventManager weakEventManager = new WeakEventManager(); + readonly Func canExecute; + readonly DelegateWeakEventManager weakEventManager = new DelegateWeakEventManager(); - int executionCount; + volatile int executionCount; /// /// Initializes BaseCommand /// /// /// - public BaseCommand(Func canExecute, bool allowsMultipleExecutions) + protected private BaseCommand(Func? canExecute, bool allowsMultipleExecutions) { this.canExecute = canExecute ?? (_ => true); AllowsMultipleExecutions = allowsMultipleExecutions; @@ -51,12 +51,12 @@ protected int ExecutionCount get => executionCount; set { - var shouldRaiseCanExecuteChanged = AllowsMultipleExecutions switch + var shouldRaiseCanExecuteChanged = (AllowsMultipleExecutions, executionCount, value) switch { - true => false, - false when executionCount is 0 && value > 0 => true, - false when executionCount > 0 && value is 0 => true, - false => false + (true, _, _) => false, + (false, 0, >0) => true, + (false, >0, 0) => true, + (false, _, _) => false }; executionCount = value; @@ -71,7 +71,7 @@ protected int ExecutionCount /// /// true, if this command can be executed; otherwise, false. /// Data used by the command. If the command does not require data to be passed, this object can be set to null. - public bool CanExecute(TCanExecute parameter) => (AllowsMultipleExecutions, IsExecuting) switch + public bool CanExecute(TCanExecute? parameter) => (AllowsMultipleExecutions, IsExecuting) switch { (true, _) => canExecute(parameter), (false, true) => false, diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/ObservableObject.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/ObservableObject.shared.cs index cfffa2253..06bcc4040 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/ObservableObject.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/ObservableObject.shared.cs @@ -9,7 +9,7 @@ namespace Xamarin.CommunityToolkit.ObjectModel { /// - /// Observable object with INotifyPropertyChanged implemented + /// Observable object with INotifyPropertyChanged implemented using WeakEventManager /// public abstract class ObservableObject : INotifyPropertyChanged { @@ -37,7 +37,7 @@ public abstract class ObservableObject : INotifyPropertyChanged protected virtual bool SetProperty( ref T backingStore, T value, - [CallerMemberName] string propertyName = "", + [CallerMemberName] string? propertyName = "", Action? onChanging = null, Action? onChanged = null, Func? validateValue = null) @@ -61,7 +61,7 @@ public abstract class ObservableObject : INotifyPropertyChanged /// Raises the property changed event. /// /// Property name. - protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "") => + protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = "") => weakEventManager.RaiseEvent(this, new PropertyChangedEventArgs(propertyName), nameof(PropertyChanged)); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/ObservableRangeCollection.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/ObservableRangeCollection.shared.cs index 9f2d99521..39c54da4d 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/ObservableRangeCollection.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/ObjectModel/ObservableRangeCollection.shared.cs @@ -157,7 +157,7 @@ void RaiseChangeNotificationEvents(NotifyCollectionChangedAction action, List OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count))); OnPropertyChanged(new PropertyChangedEventArgs("Item[]")); - if (changedItems is null) + if (changedItems == null) OnCollectionChanged(new NotifyCollectionChangedEventArgs(action)); else OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, changedItems: changedItems, startingIndex: startingIndex)); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/PlatformConfiguration/WindowsSpecific/PopUp.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/PlatformConfiguration/WindowsSpecific/PopUp.shared.cs new file mode 100644 index 000000000..1fd2b4f3d --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/PlatformConfiguration/WindowsSpecific/PopUp.shared.cs @@ -0,0 +1,25 @@ +using Xamarin.CommunityToolkit.UI.Views; +using Xamarin.Forms; +using XFPC = Xamarin.Forms.PlatformConfiguration; +using XCTElement = Xamarin.CommunityToolkit.UI.Views.BasePopup; + +namespace Xamarin.CommunityToolkit.PlatformConfiguration.WindowsSpecific +{ + public static class PopUp + { + public static readonly BindableProperty BorderColorProperty = BindableProperty.Create( + "BorderColor", typeof(Color), typeof(BasePopup), default(Color)); + + public static void SetBorderColor(BindableObject element, Color color) => + element.SetValue(BorderColorProperty, color); + + public static Color GetBorderColor(BindableObject element) => + (Color)element.GetValue(BorderColorProperty); + + public static IPlatformElementConfiguration SetBorderColor(this IPlatformElementConfiguration config, Color value) + { + SetBorderColor(config.Element, value); + return config; + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/PlatformConfiguration/iOSSpecific/PopUp.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/PlatformConfiguration/iOSSpecific/PopUp.shared.cs new file mode 100644 index 000000000..165ab36d1 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/PlatformConfiguration/iOSSpecific/PopUp.shared.cs @@ -0,0 +1,36 @@ +using Xamarin.CommunityToolkit.UI.Views; +using Xamarin.Forms; +using XFPC = Xamarin.Forms.PlatformConfiguration; +using XCTElement = Xamarin.CommunityToolkit.UI.Views.BasePopup; + +namespace Xamarin.CommunityToolkit.PlatformConfiguration.iOSSpecific +{ + public static class PopUp + { + public static readonly BindableProperty ArrowDirectionProperty = BindableProperty.Create( + "ArrowDirection", typeof(PopoverArrowDirection), typeof(BasePopup), PopoverArrowDirection.None); + + public static void SetArrowDirection(BindableObject element, PopoverArrowDirection color) => + element.SetValue(ArrowDirectionProperty, color); + + public static PopoverArrowDirection GetArrowDirection(BindableObject element) => + (PopoverArrowDirection)element.GetValue(ArrowDirectionProperty); + + public static IPlatformElementConfiguration UseArrowDirection(this IPlatformElementConfiguration config, PopoverArrowDirection value) + { + SetArrowDirection(config.Element, value); + return config; + } + } + + public enum PopoverArrowDirection + { + None, + Up, + Down, + Left, + Right, + Any, + Unknown + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Startup/ToolkitPlatform.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Startup/ToolkitPlatform.android.cs new file mode 100644 index 000000000..5db84776f --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Startup/ToolkitPlatform.android.cs @@ -0,0 +1,25 @@ +using System; +using Android.Content; +using Xamarin.Forms.Platform.Android; + +namespace Xamarin.CommunityToolkit +{ + /// + /// Platform extension methods. + /// + static class ToolkitPlatform + { + /// + /// Gets the . + /// + internal static Context Context + { + get + { + var page = Forms.Application.Current.MainPage; + var renderer = page.GetRenderer(); + return renderer.View.Context ?? throw new NullReferenceException($"{nameof(Context)} cannot be null"); + } + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/AvatarView.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/AvatarView.shared.cs index d287e241a..f9fea7746 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/AvatarView.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/AvatarView.shared.cs @@ -19,9 +19,9 @@ public class AvatarView : BaseTemplatedView readonly SemaphoreSlim imageSourceSemaphore = new SemaphoreSlim(1); - CancellationTokenSource imageLoadingTokenSource; + CancellationTokenSource? imageLoadingTokenSource; - object sourceBindingContext; + object? sourceBindingContext; // Indicates if any thread already waits for the semaphore is released (0 == False | 1 == True). int isWaitingForSourceUpdateValue; @@ -134,18 +134,18 @@ public Color Color /// /// Gets or sets the that is used to try and show an avatar image. If the image could not be loaded, the will be shown. This is a bindable property. /// - public ImageSource Source + public ImageSource? Source { - get => (ImageSource)GetValue(SourceProperty); + get => (ImageSource?)GetValue(SourceProperty); set => SetValue(SourceProperty, value); } /// /// Gets or sets the text for the . Which is shown instead of the image when the is either not set or doesn't result in showing an image. This is a bindable property. /// - public string Text + public string? Text { - get => (string)GetValue(TextProperty); + get => (string?)GetValue(TextProperty); set => SetValue(TextProperty, value); } @@ -161,9 +161,9 @@ public Color TextColor /// /// Font of the on the . This is a bindable property. /// - public string FontFamily + public string? FontFamily { - get => (string)GetValue(FontFamilyProperty); + get => (string?)GetValue(FontFamilyProperty); set => SetValue(FontFamilyProperty, value); } @@ -354,23 +354,17 @@ async void OnSourcePropertyChanged(bool isBindingContextChanged) try { - var imageStreamLoadingTask = GetImageStreamLoadingTask(source, imageLoadingTokenSource.Token); - if (imageStreamLoadingTask != null) + if (source is UriImageSource uriImageSource) { - using var stream = await imageStreamLoadingTask; - if (stream != null) - { - var newStream = new MemoryStream(); - stream.CopyTo(newStream); - newStream.Position = 0; - Image.IsVisible = true; - source = ImageSource.FromStream(() => newStream); - } - else - { - Image.IsVisible = false; - source = null; - } + static async Task getStreamAsync(UriImageSource uriSource, CancellationToken token) => uriSource.Uri != null ? await uriSource.GetStreamAsync(token) : null; + + var stream = await getStreamAsync(uriImageSource, imageLoadingTokenSource.Token); + + source = stream != null + ? ImageSource.FromStream(async (token) => stream?.CanRead ?? true ? stream : (stream = await getStreamAsync(uriImageSource, token))) + : null; + + Image.IsVisible = source != null; } else Image.IsVisible = await imageSourceValidator.IsImageSourceValidAsync(source); @@ -402,20 +396,5 @@ async void OnSourcePropertyChanged(bool isBindingContextChanged) return size * .4; } - - Task GetImageStreamLoadingTask(ImageSource source, CancellationToken token) - => source switch - { - IStreamImageSource streamImageSource => streamImageSource.GetStreamAsync(token), - UriImageSource uriImageSource => uriImageSource.Uri != null - ? new UriImageSource - { - Uri = uriImageSource.Uri, - CachingEnabled = uriImageSource.CachingEnabled, - CacheValidity = uriImageSource.CacheValidity - }.GetStreamAsync(token) - : Task.FromResult(null), - _ => null - }; } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/ColorTheme.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/ColorTheme.shared.cs index 55627b1bb..b1b492901 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/ColorTheme.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/ColorTheme.shared.cs @@ -112,7 +112,6 @@ public Color GetBackgroundColor(string text) Color.FromRgb(168, 101, 30) }); - /// /// A Ocean inspirated . /// diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/IImageSourceValidator.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/IImageSourceValidator.shared.cs index 10b23478c..5b806eca4 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/IImageSourceValidator.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/IImageSourceValidator.shared.cs @@ -5,6 +5,6 @@ namespace Xamarin.CommunityToolkit.UI.Views { interface IImageSourceValidator { - Task IsImageSourceValidAsync(ImageSource source); + Task IsImageSourceValidAsync(ImageSource? source); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/ImageSourceValidator.android.ios.macos.uwp.wpf.gtk.tizen.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/ImageSourceValidator.android.ios.macos.uwp.wpf.gtk.tizen.cs index b1150dd83..ba2e17093 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/ImageSourceValidator.android.ios.macos.uwp.wpf.gtk.tizen.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/ImageSourceValidator.android.ios.macos.uwp.wpf.gtk.tizen.cs @@ -31,7 +31,7 @@ namespace Xamarin.CommunityToolkit.UI.Views { class ImageSourceValidator : IImageSourceValidator { - public async Task IsImageSourceValidAsync(ImageSource source) + public async Task IsImageSourceValidAsync(ImageSource? source) { var handler = GetHandler(source); if (handler == null) @@ -40,7 +40,7 @@ public async Task IsImageSourceValidAsync(ImageSource source) #if TIZEN return await handler.LoadImageAsync(new NImage(XForms.NativeParent), source).ConfigureAwait(false); #elif MONOANDROID - var imageSource = await handler.LoadImageAsync(source, null).ConfigureAwait(false); + var imageSource = await handler.LoadImageAsync(source, ToolkitPlatform.Context).ConfigureAwait(false); return imageSource != null; #else var imageSource = await handler.LoadImageAsync(source).ConfigureAwait(false); @@ -48,7 +48,7 @@ public async Task IsImageSourceValidAsync(ImageSource source) #endif } - IImageSourceHandler GetHandler(ImageSource source) + IImageSourceHandler? GetHandler(ImageSource? source) { if (source is UriImageSource) return new UriImageSourceHandler(); @@ -66,8 +66,14 @@ IImageSourceHandler GetHandler(ImageSource source) return new FontImageSourceHandler(); #endif - if (source is FileImageSource fileSource && File.Exists(fileSource.File)) + if (source is FileImageSource fileSource) + { +#if !MONOANDROID + if (!File.Exists(fileSource.File)) + return null; +#endif return new FileImageSourceHandler(); + } return null; } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/ImageSourceValidator.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/ImageSourceValidator.shared.cs index e0b7550a3..034088092 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/ImageSourceValidator.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/AvatarView/ImageSourceValidator.shared.cs @@ -6,7 +6,7 @@ namespace Xamarin.CommunityToolkit.UI.Views #if NETSTANDARD || __TVOS__ || __WATCHOS__ class ImageSourceValidator : IImageSourceValidator { - public Task IsImageSourceValidAsync(ImageSource source) => Task.FromResult(false); + public Task IsImageSourceValidAsync(ImageSource? source) => Task.FromResult(false); } #endif } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/BadgeView/BadgeView.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/BadgeView/BadgeView.shared.cs index cefdf0c8e..8577e8b6c 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/BadgeView/BadgeView.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/BadgeView/BadgeView.shared.cs @@ -27,9 +27,9 @@ public class BadgeView : BaseTemplatedView /// /// Gets or sets the on top of which the will be shown. This is a bindable property. /// - public View Content + public View? Content { - get => (View)GetValue(ContentProperty); + get => (View?)GetValue(ContentProperty); set => SetValue(ContentProperty, value); } @@ -67,7 +67,7 @@ public bool AutoHide set => SetValue(AutoHideProperty, value); } - static async void OnAutoHideChanged(BindableObject bindable, object oldValue, object newValue) => await (bindable as BadgeView)?.UpdateVisibilityAsync(); + static async void OnAutoHideChanged(BindableObject bindable, object oldValue, object newValue) => await ((BadgeView)bindable).UpdateVisibilityAsync(); /// /// Backing BindableProperty for the property. @@ -93,9 +93,9 @@ public bool IsAnimated /// /// Gets or sets the animation that is used when the badge is shown or hidden. The animation only shows when is set to true. This is a bindable property. /// - public IBadgeAnimation BadgeAnimation + public IBadgeAnimation? BadgeAnimation { - get => (IBadgeAnimation)GetValue(BadgeAnimationProperty); + get => (IBadgeAnimation?)GetValue(BadgeAnimationProperty); set => SetValue(BadgeAnimationProperty, value); } @@ -103,8 +103,9 @@ public IBadgeAnimation BadgeAnimation /// Backing BindableProperty for the property. /// public static new BindableProperty BackgroundColorProperty = - BindableProperty.Create(nameof(BackgroundColor), typeof(Color), typeof(BadgeView), defaultValue: Color.Default); - + BindableProperty.Create(nameof(BackgroundColor), typeof(Color), typeof(BadgeView), defaultValue: Color.Default, + propertyChanged: OnLayoutPropertyChanged); + /// /// Gets or sets the background of the badge. This is a bindable property. /// @@ -258,35 +259,31 @@ protected override void OnControlInitialized(Grid control) control.Children.Add(BadgeIndicatorContainer); } - static ContentPresenter CreateContentElement() - => new ContentPresenter - { - HorizontalOptions = LayoutOptions.Start, - VerticalOptions = LayoutOptions.Start - }; - - static Grid CreateIndicatorContainerElement() - => new Grid - { - HorizontalOptions = LayoutOptions.Start, - VerticalOptions = LayoutOptions.Start, - IsVisible = false - }; - - static Frame CreateIndicatorBackgroundElement() - => new Frame - { - CornerRadius = Device.RuntimePlatform == Device.Android ? 12 : 8, - Padding = 2 - }; - - static Label CreateTextElement() - => new Label - { - HorizontalOptions = LayoutOptions.Center, - VerticalOptions = LayoutOptions.Center, - Margin = new Thickness(4, 0) - }; + static ContentPresenter CreateContentElement() => new ContentPresenter + { + HorizontalOptions = LayoutOptions.Start, + VerticalOptions = LayoutOptions.Start + }; + + static Grid CreateIndicatorContainerElement() => new Grid + { + HorizontalOptions = LayoutOptions.Start, + VerticalOptions = LayoutOptions.Start, + IsVisible = false + }; + + static Frame CreateIndicatorBackgroundElement() => new Frame + { + CornerRadius = Device.RuntimePlatform == Device.Android ? 12 : 8, + Padding = 2 + }; + + static Label CreateTextElement() => new Label + { + HorizontalOptions = LayoutOptions.Center, + VerticalOptions = LayoutOptions.Center, + Margin = new Thickness(4, 0) + }; protected override void OnBindingContextChanged() { @@ -356,8 +353,8 @@ void UpdateBadgeViewPlacement(bool force = false) var size = Math.Max(BadgeText.Height, BadgeText.Width) + Padding; BadgeIndicatorBackground.HeightRequest = size; var margins = GetMargins(size); - containerMargin = margins.Item1; - contentMargin = margins.Item2; + containerMargin = margins.ContainerMargin; + contentMargin = margins.ContentMargin; } BadgeIndicatorContainer.Margin = containerMargin; @@ -365,7 +362,7 @@ void UpdateBadgeViewPlacement(bool force = false) placementDone = true; } - Tuple GetMargins(double size) + (Thickness ContainerMargin, Thickness ContentMargin) GetMargins(double size) { double verticalMargin; double horizontalMargin; @@ -379,17 +376,20 @@ void UpdateBadgeViewPlacement(bool force = false) containerMargin = new Thickness(horizontalMargin, 0, 0, 0); contentMargin = new Thickness(0, verticalMargin, verticalMargin, 0); break; + case BadgePosition.TopLeft: verticalMargin = size / 2; containerMargin = new Thickness(0, 0, 0, 0); contentMargin = new Thickness(verticalMargin, verticalMargin, 0, 0); break; + case BadgePosition.BottomLeft: verticalMargin = size / 2; var bottomLeftverticalMargin = BadgeContent.Height - verticalMargin; containerMargin = new Thickness(0, bottomLeftverticalMargin, 0, 0); contentMargin = new Thickness(verticalMargin, 0, 0, 0); break; + case BadgePosition.BottomRight: verticalMargin = size / 2; var bottomRightverticalMargin = BadgeContent.Height - verticalMargin; @@ -398,7 +398,7 @@ void UpdateBadgeViewPlacement(bool force = false) contentMargin = new Thickness(0, 0, verticalMargin, 0); break; } - return new Tuple(containerMargin, contentMargin); + return (containerMargin, contentMargin); } async Task UpdateVisibilityAsync() @@ -425,24 +425,29 @@ async Task UpdateVisibilityAsync() if (badgeIsVisible) { BadgeIndicatorContainer.IsVisible = true; - await BadgeAnimation.OnAppearing(BadgeIndicatorContainer); + + if (BadgeAnimation != null) + await BadgeAnimation.OnAppearing(BadgeIndicatorContainer); } else { - await BadgeAnimation.OnDisappering(BadgeIndicatorContainer); + if (BadgeAnimation != null) + await BadgeAnimation.OnDisappering(BadgeIndicatorContainer); BadgeIndicatorContainer.IsVisible = false; } isVisible = badgeIsVisible; } else + { BadgeIndicatorContainer.IsVisible = badgeIsVisible; + } } - void OnBadgeTextSizeChanged(object sender, EventArgs e) + void OnBadgeTextSizeChanged(object? sender, EventArgs e) => UpdateBadgeViewPlacement(true); - void OnBadgeIndicatorContainerPropertyChanged(object sender, PropertyChangedEventArgs e) + void OnBadgeIndicatorContainerPropertyChanged(object? sender, PropertyChangedEventArgs e) => UpdateBadgeViewPlacement(true); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/BaseTemplatedView.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/BaseTemplatedView.shared.cs index 397e98325..29e99e3c3 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/BaseTemplatedView.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/BaseTemplatedView.shared.cs @@ -1,4 +1,5 @@ -using Xamarin.Forms; +using System; +using Xamarin.Forms; namespace Xamarin.CommunityToolkit.UI.Views.Internals { @@ -8,7 +9,9 @@ namespace Xamarin.CommunityToolkit.UI.Views.Internals /// The type of the control that this template will be used for public abstract class BaseTemplatedView : TemplatedView where TControl : View, new() { - protected TControl Control { get; private set; } + TControl? control; + + protected TControl Control => control ?? throw new NullReferenceException(); /// /// Constructor of @@ -19,14 +22,16 @@ public BaseTemplatedView() protected override void OnBindingContextChanged() { base.OnBindingContextChanged(); - Control.BindingContext = BindingContext; + + if (control != null) + Control.BindingContext = BindingContext; } protected override void OnChildAdded(Element child) { - if (Control == null && child is TControl content) + if (control == null && child is TControl content) { - Control = content; + control = content; OnControlInitialized(Control); } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/AutoFitTextureView.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/AutoFitTextureView.android.cs index 6723a21da..742f24bbc 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/AutoFitTextureView.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/AutoFitTextureView.android.cs @@ -1,10 +1,8 @@ using System; using Android.Content; -using Android.Graphics; using Android.Runtime; using Android.Util; using Android.Views; -using AColor = Android.Graphics.Color; namespace Xamarin.CommunityToolkit.UI.Views { @@ -18,12 +16,12 @@ public AutoFitTextureView(Context context) { } - public AutoFitTextureView(Context context, IAttributeSet attrs) + public AutoFitTextureView(Context context, IAttributeSet? attrs) : this(context, attrs, 0) { } - public AutoFitTextureView(Context context, IAttributeSet attrs, int defStyle) + public AutoFitTextureView(Context context, IAttributeSet? attrs, int defStyle) : base(context, attrs, defStyle) { } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraCaptureListener.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraCaptureListener.android.cs index 911681df2..796197773 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraCaptureListener.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraCaptureListener.android.cs @@ -5,7 +5,7 @@ namespace Xamarin.CommunityToolkit.UI.Views { class CameraCaptureListener : CameraCaptureSession.CaptureCallback { - public Action OnCompleted; + public Action? OnCompleted; public override void OnCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) => OnCompleted?.Invoke(result); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraCaptureStateListener.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraCaptureStateListener.android.cs index 3256bdcc4..c805e2cc4 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraCaptureStateListener.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraCaptureStateListener.android.cs @@ -5,8 +5,8 @@ namespace Xamarin.CommunityToolkit.UI.Views { class CameraCaptureStateListener : CameraCaptureSession.StateCallback { - public Action OnConfigureFailedAction; - public Action OnConfiguredAction; + public Action? OnConfigureFailedAction; + public Action? OnConfiguredAction; public override void OnConfigureFailed(CameraCaptureSession session) => OnConfigureFailedAction?.Invoke(session); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraFragment.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraFragment.android.cs index 6e4284707..ef6a0e665 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraFragment.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraFragment.android.cs @@ -45,20 +45,24 @@ namespace Xamarin.CommunityToolkit.UI.Views class CameraFragment : Fragment, TextureView.ISurfaceTextureListener { // Max preview width that is guaranteed by Camera2 API - const int MAX_PREVIEW_HEIGHT = 1080; + const int maxPreviewHeight = 1080; + // Max preview height that is guaranteed by Camera2 API - const int MAX_PREVIEW_WIDTH = 1920; + const int maxPreviewWidth = 1920; + + readonly Java.Util.Concurrent.Semaphore captureSessionOpenCloseLock = new Java.Util.Concurrent.Semaphore(1); + readonly MediaActionSound mediaSound = new MediaActionSound(); - CameraDevice device; - CaptureRequest.Builder sessionBuilder; - CameraCaptureSession session; + CameraDevice? device; + CaptureRequest.Builder? sessionBuilder; + CameraCaptureSession? session; - AutoFitTextureView texture; - ImageReader photoReader; - MediaRecorder mediaRecorder; + AutoFitTextureView? texture; + ImageReader? photoReader; + MediaRecorder? mediaRecorder; bool audioPermissionsGranted; bool cameraPermissionsGranted; - ASize previewSize, videoSize, photoSize; + ASize? previewSize, videoSize, photoSize; int sensorOrientation; LensFacing cameraType; @@ -67,34 +71,21 @@ class CameraFragment : Fragment, TextureView.ISurfaceTextureListener bool stabilizationSupported; bool repeatingIsRunning; FlashMode flashMode; - string cameraId; - string videoFile; - Java.Util.Concurrent.Semaphore captureSessionOpenCloseLock = new Java.Util.Concurrent.Semaphore(1); - CameraTemplate cameraTemplate; - HandlerThread backgroundThread; - Handler backgroundHandler = null; + string? cameraId; + string videoFile = string.Empty; + CameraTemplate? cameraTemplate; + HandlerThread? backgroundThread; + Handler? backgroundHandler = null; float zoom = 1; - bool ZoomSupported => maxDigitalZoom != 0; - float maxDigitalZoom; - Rect activeRect; - - public bool IsRecordingVideo { get; set; } - - bool UseSystemSound { get; set; } - - CameraManager manager; + Rect? activeRect; - CameraManager Manager => manager ??= (CameraManager)Context.GetSystemService(Context.CameraService); + CameraManager? manager; - MediaActionSound mediaSound; - - MediaActionSound MediaSound => mediaSound ??= new MediaActionSound(); - - TaskCompletionSource initTaskSource; - TaskCompletionSource permissionsRequested; + TaskCompletionSource? initTaskSource; + TaskCompletionSource? permissionsRequested; public CameraFragment() { @@ -121,14 +112,22 @@ bool Available get => Element?.IsAvailable ?? false; set { - if (Element?.IsAvailable != value) + if (Element != null && Element.IsAvailable != value) Element.IsAvailable = value; } } - public CameraView Element { get; set; } + public bool IsRecordingVideo { get; set; } + + bool UseSystemSound { get; set; } + + public CameraView? Element { get; set; } - public override AView OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) => + CameraManager Manager => manager ??= (CameraManager)(Context.GetSystemService(Context.CameraService) ?? throw new NullReferenceException()); + + bool ZoomSupported => maxDigitalZoom != 0; + + public override AView? OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) => inflater.Inflate(Resource.Layout.CameraFragment, null); public override void OnViewCreated(AView view, Bundle savedInstanceState) => @@ -138,8 +137,10 @@ public override async void OnResume() { base.OnResume(); StartBackgroundThread(); - if (texture is null) + + if (texture == null) return; + if (texture.IsAvailable) { UpdateBackgroundColor(); @@ -161,7 +162,7 @@ void StartBackgroundThread() { backgroundThread = new HandlerThread("CameraBackground"); backgroundThread.Start(); - backgroundHandler = new Handler(backgroundThread.Looper); + backgroundHandler = new Handler(backgroundThread.Looper ?? throw new NullReferenceException()); } void StopBackgroundThread() @@ -200,39 +201,44 @@ public async Task RetrieveCameraDevice(bool force = false) IsBusy = true; cameraId = GetCameraId(); - if (string.IsNullOrEmpty(cameraId)) + if (cameraId == null || string.IsNullOrEmpty(cameraId)) { IsBusy = false; captureSessionOpenCloseLock.Release(); // _texture.ClearCanvas(Element.BackgroundColor.ToAndroid()); // HANG after select valid camera... - Element.RaiseMediaCaptureFailed($"No {Element.CameraOptions} camera found"); + Element?.RaiseMediaCaptureFailed($"No {Element.CameraOptions} camera found"); } else { try { var characteristics = Manager.GetCameraCharacteristics(cameraId); - var map = (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap); + var map = (StreamConfigurationMap)(characteristics?.Get(CameraCharacteristics.ScalerStreamConfigurationMap) ?? throw new NullReferenceException()); flashSupported = characteristics.Get(CameraCharacteristics.FlashInfoAvailable) == Java.Lang.Boolean.True; stabilizationSupported = false; var stabilizationModes = characteristics.Get(CameraCharacteristics.ControlAvailableVideoStabilizationModes); - if (stabilizationModes != null) + + if (stabilizationModes is IEnumerable modes) { - var modes = (int[])stabilizationModes; foreach (var mode in modes) { if (mode == (int)ControlVideoStabilizationMode.On) stabilizationSupported = true; } } - Element.MaxZoom = maxDigitalZoom = (float)characteristics.Get(CameraCharacteristics.ScalerAvailableMaxDigitalZoom); - activeRect = (Rect)characteristics.Get(CameraCharacteristics.SensorInfoActiveArraySize); - sensorOrientation = (int)characteristics.Get(CameraCharacteristics.SensorOrientation); + + if (Element != null) + Element.MaxZoom = maxDigitalZoom = (float)(characteristics.Get(CameraCharacteristics.ScalerAvailableMaxDigitalZoom) ?? throw new NullReferenceException()); + + activeRect = (Rect)(characteristics.Get(CameraCharacteristics.SensorInfoActiveArraySize) ?? throw new NullReferenceException()); + sensorOrientation = (int)(characteristics.Get(CameraCharacteristics.SensorOrientation) ?? throw new NullReferenceException()); var displaySize = new APoint(); - Activity.WindowManager.DefaultDisplay.GetSize(displaySize); + Activity.WindowManager?.DefaultDisplay?.GetSize(displaySize); + + _ = texture ?? throw new NullReferenceException(); var rotatedViewWidth = texture.Width; var rotatedViewHeight = texture.Height; var maxPreviewWidth = displaySize.X; @@ -246,33 +252,33 @@ public async Task RetrieveCameraDevice(bool force = false) maxPreviewHeight = displaySize.X; } - if (maxPreviewHeight > MAX_PREVIEW_HEIGHT) + if (maxPreviewHeight > CameraFragment.maxPreviewHeight) { - maxPreviewHeight = MAX_PREVIEW_HEIGHT; + maxPreviewHeight = CameraFragment.maxPreviewHeight; } - if (maxPreviewWidth > MAX_PREVIEW_WIDTH) + if (maxPreviewWidth > CameraFragment.maxPreviewWidth) { - maxPreviewWidth = MAX_PREVIEW_WIDTH; + maxPreviewWidth = CameraFragment.maxPreviewWidth; } photoSize = GetMaxSize(map.GetOutputSizes((int)ImageFormatType.Jpeg)); videoSize = GetMaxSize(map.GetOutputSizes(Class.FromType(typeof(MediaRecorder)))); previewSize = ChooseOptimalSize( - map.GetOutputSizes(Class.FromType(typeof(SurfaceTexture))), + map.GetOutputSizes(Class.FromType(typeof(SurfaceTexture))) ?? throw new NullReferenceException(), rotatedViewWidth, rotatedViewHeight, maxPreviewWidth, maxPreviewHeight, cameraTemplate == CameraTemplate.Record ? videoSize : photoSize); - cameraType = (LensFacing)(int)characteristics.Get(CameraCharacteristics.LensFacing); + cameraType = (LensFacing)(int)(characteristics.Get(CameraCharacteristics.LensFacing) ?? throw new NullReferenceException()); - if (Resources.Configuration.Orientation == AOrientation.Landscape) + if (Resources.Configuration?.Orientation == AOrientation.Landscape) texture.SetAspectRatio(previewSize.Width, previewSize.Height); else texture.SetAspectRatio(previewSize.Height, previewSize.Width); - initTaskSource = new TaskCompletionSource(); + initTaskSource = new TaskCompletionSource(); Manager.OpenCamera( cameraId, @@ -318,16 +324,11 @@ public async Task RetrieveCameraDevice(bool force = false) public void UpdateCaptureOptions() { - switch (Element.CaptureMode) + cameraTemplate = Element?.CaptureMode switch { - default: - case CameraCaptureMode.Photo: - cameraTemplate = CameraTemplate.Preview; - break; - case CameraCaptureMode.Video: - cameraTemplate = CameraTemplate.Record; - break; - } + CameraCaptureMode.Video => CameraTemplate.Record, + _ => CameraTemplate.Preview, + }; } public void TakePhoto() @@ -337,12 +338,12 @@ public void TakePhoto() try { - if (device != null) + if (device != null && session != null && sessionBuilder != null && photoReader?.Surface != null) { session.StopRepeating(); repeatingIsRunning = false; sessionBuilder.AddTarget(photoReader.Surface); - sessionBuilder.Set(CaptureRequest.FlashMode, (int)flashMode); + sessionBuilder.Set(CaptureRequest.FlashMode ?? throw new NullReferenceException(), (int)flashMode); /*sessionBuilder.Set(CaptureRequest.JpegOrientation, GetJpegOrientation());*/ session.Capture(sessionBuilder.Build(), null, null); sessionBuilder.RemoveTarget(photoReader.Surface); @@ -355,11 +356,11 @@ public void TakePhoto() } } - void OnPhoto(object sender, Tuple tuple) => + void OnPhoto(object? sender, (string?, byte[], int) tuple) => Device.BeginInvokeOnMainThread(() => Element?.RaiseMediaCaptured(new MediaCapturedEventArgs(tuple.Item1, tuple.Item2, tuple.Item3))); - void OnVideo(object sender, string path) => + void OnVideo(object? sender, string path) => Device.BeginInvokeOnMainThread(() => Element?.RaiseMediaCaptured(new MediaCapturedEventArgs(path))); @@ -367,12 +368,13 @@ void SetupImageReader() { DisposeImageReader(); + _ = photoSize ?? throw new NullReferenceException(); photoReader = ImageReader.NewInstance(photoSize.Width, photoSize.Height, ImageFormatType.Jpeg, maxImages: 1); var readerListener = new ImageAvailableListener(); readerListener.Photo += (_, bytes) => { - string filePath = null; + string? filePath = null; // Calculate image rotation based on sensor and device orientation var rotation = GetRotationCompensation(); @@ -385,24 +387,28 @@ void SetupImageReader() File.WriteAllBytes(filePath, bytes); } Sound(MediaActionSoundType.ShutterClick); - OnPhoto(this, new Tuple(filePath, Element.SavePhotoToFile ? null : bytes));*/ + OnPhoto(this, (filePath, Element.SavePhotoToFile ? null : bytes);*/ Sound(MediaActionSoundType.ShutterClick); - OnPhoto(this, new Tuple(filePath, bytes, rotation)); + OnPhoto(this, (filePath, bytes, rotation)); }; photoReader.SetOnImageAvailableListener(readerListener, backgroundHandler); } - private int GetRotationCompensation() + int GetRotationCompensation() { + _ = cameraId ?? throw new NullReferenceException(); + var rotationCompensation = GetDisplayRotationDegrees(); - var c = Manager.GetCameraCharacteristics(cameraId); + var cameraCharacteristics = Manager.GetCameraCharacteristics(cameraId); + // Get the device's sensor orientation. - var sensorOrientation = (int)c.Get(CameraCharacteristics.SensorOrientation); + var sensorOrientation = (int)(cameraCharacteristics.Get(CameraCharacteristics.SensorOrientation) ?? throw new NullReferenceException()); + var lensFacing = (Integer)(cameraCharacteristics.Get(CameraCharacteristics.LensFacing) ?? throw new NullReferenceException()); - var facingFront = ((Integer)c.Get(CameraCharacteristics.LensFacing)).IntValue() == (int)LensFacing.Front; - if (facingFront) + var isfacingFront = lensFacing.IntValue() == (int)LensFacing.Front; + if (isfacingFront) { rotationCompensation = (sensorOrientation + rotationCompensation) % 360; } @@ -420,16 +426,22 @@ void SetupMediaRecorder(Surface previewSurface) mediaRecorder = new MediaRecorder(); mediaRecorder.SetPreviewDisplay(previewSurface); + if (audioPermissionsGranted) mediaRecorder.SetAudioSource(AudioSource.Camcorder); + mediaRecorder.SetVideoSource(AVideoSource.Surface); var profile = GetCamcoderProfile(); if (profile != null) + { mediaRecorder.SetProfile(profile); + } else { + _ = videoSize ?? throw new NullReferenceException(); + mediaRecorder.SetOutputFormat(OutputFormat.Mpeg4); mediaRecorder.SetVideoEncodingBitRate(10000000); mediaRecorder.SetVideoFrameRate(30); @@ -447,7 +459,7 @@ void SetupMediaRecorder(Surface previewSurface) mediaRecorder.Prepare(); } - CamcorderProfile GetCamcoderProfile() + CamcorderProfile? GetCamcoderProfile() { var cameraId = Convert.ToInt32(this.cameraId); if (CamcorderProfile.HasProfile(cameraId, CamcorderQuality.HighSpeed1080p)) @@ -467,7 +479,9 @@ CamcorderProfile GetCamcoderProfile() public void StartRecord() { if (IsBusy) + { return; + } else if (IsRecordingVideo) { Element?.RaiseMediaCaptureFailed("Video already recording."); @@ -499,7 +513,7 @@ public void StartRecord() } } - public async void StopRecord() + public async Task StopRecord() { if (IsBusy || !IsRecordingVideo || session == null || mediaRecorder == null) return; @@ -529,7 +543,10 @@ async Task PrepareSession() { CloseSession(); - sessionBuilder = device.CreateCaptureRequest(cameraTemplate); + if (device == null || cameraTemplate is not CameraTemplate cameraTemplate_nonNull) + throw new NullReferenceException(); + + sessionBuilder = device.CreateCaptureRequest(cameraTemplate_nonNull); SetFlash(); SetVideoStabilization(); @@ -538,32 +555,40 @@ async Task PrepareSession() var surfaces = new List(); // preview texture - if (texture.IsAvailable && previewSize != null) + if (previewSize != null && texture?.IsAvailable is true) { - var texture = this.texture.SurfaceTexture; + var texture = this.texture.SurfaceTexture ?? throw new NullReferenceException(); texture.SetDefaultBufferSize(previewSize.Width, previewSize.Height); var previewSurface = new Surface(texture); surfaces.Add(previewSurface); sessionBuilder.AddTarget(previewSurface); // video mode - if (cameraTemplate == CameraTemplate.Record) + if (cameraTemplate is CameraTemplate.Record) { SetupMediaRecorder(previewSurface); + + _ = mediaRecorder ?? throw new NullReferenceException($"{nameof(mediaRecorder)} not initialized"); var mediaSurface = mediaRecorder.Surface; - surfaces.Add(mediaSurface); - sessionBuilder.AddTarget(mediaSurface); + + if (mediaSurface != null) + { + surfaces.Add(mediaSurface); + sessionBuilder.AddTarget(mediaSurface); + } } // photo mode else { SetupImageReader(); - surfaces.Add(photoReader.Surface); + + if (photoReader?.Surface != null) + surfaces.Add(photoReader.Surface); } } - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(); device.CreateCaptureSession( surfaces, @@ -572,7 +597,7 @@ async Task PrepareSession() OnConfigureFailedAction = captureSession => { tcs.SetResult(null); - Element.RaiseMediaCaptureFailed("Failed to create captire sesstion"); + Element?.RaiseMediaCaptureFailed("Failed to create captire sesstion"); }, OnConfiguredAction = captureSession => tcs.SetResult(captureSession) }, @@ -630,10 +655,10 @@ public void UpdateRepeatingRequest() if (repeatingIsRunning) session.StopRepeating(); - sessionBuilder.Set(CaptureRequest.ControlMode, (int)ControlMode.Auto); - sessionBuilder.Set(CaptureRequest.ControlAeMode, (int)ControlAEMode.On); + sessionBuilder.Set(CaptureRequest.ControlMode ?? throw new NullReferenceException(), (int)ControlMode.Auto); + sessionBuilder.Set(CaptureRequest.ControlAeMode ?? throw new NullReferenceException(), (int)ControlAEMode.On); if (cameraTemplate == CameraTemplate.Record) - sessionBuilder.Set(CaptureRequest.FlashMode, (int)flashMode); + sessionBuilder.Set(CaptureRequest.FlashMode ?? throw new NullReferenceException(), (int)flashMode); session.SetRepeatingRequest(sessionBuilder.Build(), listener: null, backgroundHandler); repeatingIsRunning = true; @@ -687,17 +712,20 @@ public void CloseDevice() } } - void UpdateBackgroundColor() => - View?.SetBackgroundColor(Element.BackgroundColor.ToAndroid()); + void UpdateBackgroundColor() + { + if (Element != null) + View?.SetBackgroundColor(Element.BackgroundColor.ToAndroid()); + } public void SetFlash() { if (!flashSupported) return; - flashMode = Element.FlashMode switch + flashMode = Element?.FlashMode switch { - CameraFlashMode.Off => FlashMode.Off, + CameraFlashMode.Off or null => FlashMode.Off, CameraFlashMode.Torch => FlashMode.Torch, _ => FlashMode.Single, }; @@ -707,35 +735,37 @@ public void SetVideoStabilization() { if (sessionBuilder == null || !stabilizationSupported) return; - sessionBuilder.Set(CaptureRequest.ControlVideoStabilizationMode, - (int)(Element.VideoStabilization ? ControlVideoStabilizationMode.On : ControlVideoStabilizationMode.Off)); + sessionBuilder.Set(CaptureRequest.ControlVideoStabilizationMode ?? throw new NullReferenceException(), + (int)((Element?.VideoStabilization ?? false) ? ControlVideoStabilizationMode.On : ControlVideoStabilizationMode.Off)); } public void ApplyZoom() { + _ = Element ?? throw new NullReferenceException(); + zoom = (float)System.Math.Max(1f, System.Math.Min(Element.Zoom, maxDigitalZoom)); if (ZoomSupported) - sessionBuilder?.Set(CaptureRequest.ScalerCropRegion, GetZoomRect()); + sessionBuilder?.Set(CaptureRequest.ScalerCropRegion ?? throw new NullReferenceException(), GetZoomRect()); } - string GetCameraId() + string? GetCameraId() { var cameraIdList = Manager.GetCameraIdList(); if (cameraIdList.Length == 0) return null; - string FilterCameraByLens(LensFacing lensFacing) + string? FilterCameraByLens(LensFacing lensFacing) { foreach (var id in cameraIdList) { var characteristics = Manager.GetCameraCharacteristics(id); - if (lensFacing == (LensFacing)(int)characteristics.Get(CameraCharacteristics.LensFacing)) + if (lensFacing == (LensFacing)(int)(characteristics?.Get(CameraCharacteristics.LensFacing) ?? throw new NullReferenceException())) return id; } return null; } - return Element.CameraOptions switch + return Element?.CameraOptions switch { CameraOptions.Front => FilterCameraByLens(LensFacing.Front), CameraOptions.Back => FilterCameraByLens(LensFacing.Back), @@ -745,23 +775,23 @@ string FilterCameraByLens(LensFacing lensFacing) } #region TextureView.ISurfaceTextureListener - async void TextureView.ISurfaceTextureListener.OnSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) + async void TextureView.ISurfaceTextureListener.OnSurfaceTextureAvailable(SurfaceTexture? surface, int width, int height) { UpdateBackgroundColor(); UpdateCaptureOptions(); await RetrieveCameraDevice(); } - void TextureView.ISurfaceTextureListener.OnSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) => + void TextureView.ISurfaceTextureListener.OnSurfaceTextureSizeChanged(SurfaceTexture? surface, int width, int height) => ConfigureTransform(width, height); - bool TextureView.ISurfaceTextureListener.OnSurfaceTextureDestroyed(SurfaceTexture surface) + bool TextureView.ISurfaceTextureListener.OnSurfaceTextureDestroyed(SurfaceTexture? surface) { CloseDevice(); return true; } - void TextureView.ISurfaceTextureListener.OnSurfaceTextureUpdated(SurfaceTexture surface) + void TextureView.ISurfaceTextureListener.OnSurfaceTextureUpdated(SurfaceTexture? surface) { } #endregion @@ -776,7 +806,7 @@ async Task RequestCameraPermissions() cameraPermissionsGranted = ContextCompat.CheckSelfPermission(Context, Manifest.Permission.Camera) == Permission.Granted; if (!cameraPermissionsGranted) permissionsToRequest.Add(Manifest.Permission.Camera); - if (Element.CaptureMode == CameraCaptureMode.Video) + if (Element?.CaptureMode == CameraCaptureMode.Video) { audioPermissionsGranted = ContextCompat.CheckSelfPermission(Context, Manifest.Permission.RecordAudio) == Permission.Granted; if (!audioPermissionsGranted) @@ -804,14 +834,14 @@ public override void OnRequestPermissionsResult(int requestCode, string[] permis { cameraPermissionsGranted = grantResults[i] == Permission.Granted; if (!cameraPermissionsGranted) - Element.RaiseMediaCaptureFailed($"No permission to use the camera."); + Element?.RaiseMediaCaptureFailed($"No permission to use the camera."); } else if (permissions[i] == Manifest.Permission.RecordAudio) { audioPermissionsGranted = grantResults[i] == Permission.Granted; if (!audioPermissionsGranted) { - Element.RaiseMediaCaptureFailed($"No permission to record audio."); + Element?.RaiseMediaCaptureFailed($"No permission to record audio."); } } } @@ -820,7 +850,7 @@ public override void OnRequestPermissionsResult(int requestCode, string[] permis #endregion #region Helpers - void LogError(string desc, Java.Lang.Exception ex = null) + void LogError(string desc, Java.Lang.Exception? ex = null) { var newLine = System.Environment.NewLine; var sb = new StringBuilder(desc); @@ -868,60 +898,66 @@ string ConstructMediaFilename(string prefix, string extension) { // "To improve user privacy, direct access to shared/external storage devices is deprecated" // Env.GetExternalStoragePublicDirectory(Env.DirectoryDcim).AbsolutePath - var path = Context.GetExternalFilesDir(Env.DirectoryDcim).AbsolutePath; + var path = Context.GetExternalFilesDir(Env.DirectoryDcim)?.AbsolutePath ?? throw new NullReferenceException(); + if (!Directory.Exists(path)) Directory.CreateDirectory(path); + var fileName = DateTime.Now.ToString("yyyyddMM_HHmmss"); + if (!string.IsNullOrEmpty(prefix)) fileName = $"{prefix}_{fileName}"; + return System.IO.Path.Combine(path, $"{fileName}.{extension}"); } - Rect GetZoomRect() + Rect? GetZoomRect() { if (activeRect == null) return null; + var width = activeRect.Width(); var heigth = activeRect.Height(); var newWidth = (int)(width / zoom); var newHeight = (int)(heigth / zoom); var x = (width - newWidth) / 2; var y = (heigth - newHeight) / 2; + return new Rect(x, y, x + newWidth, y + newHeight); } - SurfaceOrientation GetDisplayRotation() - => App.Context.GetSystemService(Context.WindowService).JavaCast().DefaultDisplay.Rotation; + SurfaceOrientation? GetDisplayRotation() + => App.Context.GetSystemService(Context.WindowService).JavaCast()?.DefaultDisplay?.Rotation; - int GetDisplayRotationDegrees() => - GetDisplayRotation() switch - { - SurfaceOrientation.Rotation90 => 90, - SurfaceOrientation.Rotation180 => 180, - SurfaceOrientation.Rotation270 => 270, - _ => 0, - }; + int GetDisplayRotationDegrees() => GetDisplayRotation() switch + { + SurfaceOrientation.Rotation90 => 90, + SurfaceOrientation.Rotation180 => 180, + SurfaceOrientation.Rotation270 => 270, + _ => 0, + }; - int GetJpegRotationDegrees() => - GetDisplayRotation() switch - { - SurfaceOrientation.Rotation90 => 0, - SurfaceOrientation.Rotation180 => 270, - SurfaceOrientation.Rotation270 => 180, - _ => 90, - }; + int GetJpegRotationDegrees() => GetDisplayRotation() switch + { + SurfaceOrientation.Rotation90 => 0, + SurfaceOrientation.Rotation180 => 270, + SurfaceOrientation.Rotation270 => 180, + _ => 90, + }; - int GetPreviewOrientation() => - GetDisplayRotation() switch - { - SurfaceOrientation.Rotation90 => 270, - SurfaceOrientation.Rotation180 => 180, - SurfaceOrientation.Rotation270 => 90, - _ => 0, - }; + int GetPreviewOrientation() => GetDisplayRotation() switch + { + SurfaceOrientation.Rotation90 => 270, + SurfaceOrientation.Rotation180 => 180, + SurfaceOrientation.Rotation270 => 90, + _ => 0, + }; - public void ConfigureTransform() => + public void ConfigureTransform() + { + _ = texture ?? throw new NullReferenceException(); ConfigureTransform(texture.Width, texture.Height); + } void ConfigureTransform(int viewWidth, int viewHeight) { @@ -930,7 +966,7 @@ void ConfigureTransform(int viewWidth, int viewHeight) if (texture == null || previewSize == null || activity == null) return; - var rotation = (int)activity.WindowManager.DefaultDisplay.Rotation; + var rotation = (int?)activity.WindowManager?.DefaultDisplay?.Rotation; var matrix = new Matrix(); var viewRect = new RectF(0, 0, viewWidth, viewHeight); var bufferRect = new RectF(0, 0, previewSize.Height, previewSize.Width); @@ -943,7 +979,7 @@ void ConfigureTransform(int viewWidth, int viewHeight) matrix.SetRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.Fill); var scale = System.Math.Max((float)viewHeight / previewSize.Height, (float)viewWidth / previewSize.Width); matrix.PostScale(scale, scale, centerX, centerY); - matrix.PostRotate(90 * (rotation - 2), centerX, centerY); + matrix.PostRotate(90 * (rotation.Value - 2), centerX, centerY); } else if (rotation == (int)SurfaceOrientation.Rotation180) matrix.PostRotate(180, centerX, centerY); @@ -965,11 +1001,12 @@ void Sound(MediaActionSoundType soundType) mediaSound.Play(soundType); } - ASize GetMaxSize(ASize[] imageSizes) + ASize GetMaxSize(ASize[]? imageSizes) { - ASize maxSize = null; + ASize? maxSize = null; long maxPixels = 0; - for (var i = 0; i < imageSizes.Length; i++) + + for (var i = 0; i < imageSizes?.Length; i++) { long currentPixels = imageSizes[i].Width * imageSizes[i].Height; if (currentPixels > maxPixels) @@ -978,7 +1015,8 @@ ASize GetMaxSize(ASize[] imageSizes) maxPixels = currentPixels; } } - return maxSize; + + return maxSize ?? throw new NullReferenceException(); } // chooses the smallest one whose width and height are at least as large as the respective requested values @@ -986,8 +1024,10 @@ ASize ChooseOptimalSize(ASize[] choices, int width, int height, int maxWidth, in { var bigEnough = new List(); var notBigEnough = new List(); + var w = aspectRatio.Width; var h = aspectRatio.Height; + foreach (var option in choices) { if (option.Width <= maxWidth && option.Height <= maxHeight && diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraStateListener.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraStateListener.android.cs index d8c051be1..b5423fa06 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraStateListener.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraStateListener.android.cs @@ -1,22 +1,21 @@ using System; -using System.Collections.Generic; using Android.Hardware.Camera2; namespace Xamarin.CommunityToolkit.UI.Views { class CameraStateListener : CameraDevice.StateCallback { - public Action OnOpenedAction; - public Action OnDisconnectedAction; - public Action OnErrorAction; - public Action OnClosedAction; + public Action? OnOpenedAction; + public Action? OnDisconnectedAction; + public Action? OnErrorAction; + public Action? OnClosedAction; public override void OnOpened(CameraDevice camera) => OnOpenedAction?.Invoke(camera); public override void OnDisconnected(CameraDevice camera) => OnDisconnectedAction?.Invoke(camera); - public override void OnError(CameraDevice camera, CameraError error) => OnErrorAction(camera, error); + public override void OnError(CameraDevice camera, CameraError error) => OnErrorAction?.Invoke(camera, error); - public override void OnClosed(CameraDevice camera) => OnClosedAction(camera); + public override void OnClosed(CameraDevice camera) => OnClosedAction?.Invoke(camera); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraViewRenderer.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraViewRenderer.android.cs index 198a4a725..39c54323e 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraViewRenderer.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/CameraViewRenderer.android.cs @@ -17,6 +17,7 @@ using Xamarin.Forms; using Xamarin.CommunityToolkit.UI.Views; using System.Reflection; +using System.Threading.Tasks; [assembly: ExportRenderer(typeof(CameraView), typeof(CameraViewRenderer))] @@ -24,17 +25,19 @@ namespace Xamarin.CommunityToolkit.UI.Views { public class CameraViewRenderer : FrameLayout, IVisualElementRenderer, IViewRenderer { + readonly MotionEventHelper motionEventHelper; + int? defaultLabelFor; bool disposed; - CameraView element; - VisualElementTracker visualElementTracker; - VisualElementRenderer visualElementRenderer; - readonly MotionEventHelper motionEventHelper; - FragmentManager fragmentManager; + CameraView? element; + VisualElementTracker? visualElementTracker; + VisualElementRenderer? visualElementRenderer; + + FragmentManager? fragmentManager; FragmentManager FragmentManager => fragmentManager ??= Context.GetFragmentManager(); - CameraFragment camerafragment; + CameraFragment? camerafragment; public CameraViewRenderer(Context context) : base(context) @@ -43,12 +46,14 @@ public CameraViewRenderer(Context context) visualElementRenderer = new VisualElementRenderer(this); } - public event EventHandler ElementChanged; + public event EventHandler? ElementChanged; - public event EventHandler ElementPropertyChanged; + public event EventHandler? ElementPropertyChanged; - async void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + async void OnElementPropertyChanged(object? sender, PropertyChangedEventArgs e) { + _ = camerafragment ?? throw new NullReferenceException(); + ElementPropertyChanged?.Invoke(this, e); switch (e.PropertyName) @@ -62,7 +67,7 @@ async void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) break; case nameof(CameraView.FlashMode): camerafragment.SetFlash(); - if (Element.CaptureMode == CameraCaptureMode.Video) + if (Element?.CaptureMode == CameraCaptureMode.Video) camerafragment.UpdateRepeatingRequest(); break; case nameof(CameraView.Zoom): @@ -71,7 +76,7 @@ async void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) break; case nameof(CameraView.VideoStabilization): camerafragment.SetVideoStabilization(); - if (Element.CaptureMode == CameraCaptureMode.Video) + if (Element?.CaptureMode == CameraCaptureMode.Video) camerafragment.UpdateRepeatingRequest(); break; @@ -86,15 +91,16 @@ async void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) } } - void OnElementChanged(ElementChangedEventArgs e) + void OnElementChanged(ElementChangedEventArgs e) { - CameraFragment newfragment = null; + CameraFragment? newfragment = null; if (e.OldElement != null) { e.OldElement.PropertyChanged -= OnElementPropertyChanged; e.OldElement.ShutterClicked -= OnShutterClicked; - camerafragment.Dispose(); + camerafragment?.Dispose(); + camerafragment = null; } if (e.NewElement != null) @@ -115,7 +121,7 @@ void OnElementChanged(ElementChangedEventArgs e) ElementChanged?.Invoke(this, new VisualElementChangedEventArgs(e.OldElement, e.NewElement)); } - CameraView Element + CameraView? Element { get => element; set @@ -126,7 +132,7 @@ CameraView Element var oldElement = element; element = value; - OnElementChanged(new ElementChangedEventArgs(oldElement, element)); + OnElementChanged(new ElementChangedEventArgs(oldElement, element)); // this is just used to set ID's to the NativeViews along time ago for UITest with Test Cloud // https://discordapp.com/channels/732297728826277939/738043671575920700/747629874709266449 @@ -134,9 +140,9 @@ CameraView Element } } - public override bool OnTouchEvent(MotionEvent e) + public override bool OnTouchEvent(MotionEvent? e) { - if (visualElementRenderer.OnTouchEvent(e) || base.OnTouchEvent(e)) + if (visualElementRenderer?.OnTouchEvent(e) is true || base.OnTouchEvent(e)) return true; return motionEventHelper.HandleMotionEvent(Parent, e); @@ -147,7 +153,8 @@ protected override void Dispose(bool disposing) if (disposed) return; - camerafragment.Dispose(); + camerafragment?.Dispose(); + camerafragment = null; disposed = true; @@ -180,27 +187,27 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - void OnShutterClicked(object sender, EventArgs e) + async void OnShutterClicked(object? sender, EventArgs e) { - switch (Element.CaptureMode) + switch (Element?.CaptureMode) { default: case CameraCaptureMode.Default: case CameraCaptureMode.Photo: - camerafragment.TakePhoto(); + camerafragment?.TakePhoto(); break; case CameraCaptureMode.Video: - if (!camerafragment.IsRecordingVideo) - camerafragment.StartRecord(); + if (camerafragment?.IsRecordingVideo is false) + camerafragment?.StartRecord(); else - camerafragment.StopRecord(); + await (camerafragment?.StopRecord() ?? Task.CompletedTask); break; } } void IViewRenderer.MeasureExactly() => MeasureExactly(this, Element, Context); - static void MeasureExactly(AView control, VisualElement element, Context context) + static void MeasureExactly(AView control, VisualElement? element, Context? context) { if (control == null || element == null) return; @@ -221,11 +228,11 @@ static void MeasureExactly(AView control, VisualElement element, Context context } #region IVisualElementRenderer - VisualElement IVisualElementRenderer.Element => Element; + VisualElement? IVisualElementRenderer.Element => Element; - ViewGroup IVisualElementRenderer.ViewGroup => null; + ViewGroup? IVisualElementRenderer.ViewGroup => null; - VisualElementTracker IVisualElementRenderer.Tracker => visualElementTracker; + VisualElementTracker? IVisualElementRenderer.Tracker => visualElementTracker; AView IVisualElementRenderer.View => this; @@ -238,7 +245,7 @@ SizeRequest IVisualElementRenderer.GetDesiredSize(int widthConstraint, int heigh void IVisualElementRenderer.SetElement(VisualElement element) { - if (!(element is CameraView camera)) + if (element is not CameraView camera) throw new ArgumentException($"{nameof(element)} must be of type {nameof(CameraView)}"); // Performance.Start(out var reference); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/ImageAvailableListener.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/ImageAvailableListener.android.cs index 5ae24c702..878174420 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/ImageAvailableListener.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/ImageAvailableListener.android.cs @@ -6,16 +6,22 @@ namespace Xamarin.CommunityToolkit.UI.Views { class ImageAvailableListener : Java.Lang.Object, ImageReader.IOnImageAvailableListener { - public event EventHandler Photo; + public event EventHandler? Photo; - public void OnImageAvailable(ImageReader reader) + public void OnImageAvailable(ImageReader? reader) { - AImage image = null; + AImage? image = null; try { - image = reader.AcquireNextImage(); - var buffer = image.GetPlanes()[0].Buffer; + image = reader?.AcquireNextImage(); + if (image == null) + return; + + var buffer = image.GetPlanes()?[0].Buffer; + if (buffer == null) + return; + var imageData = new byte[buffer.Capacity()]; buffer.Get(imageData); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/MotionEventHelper.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/MotionEventHelper.android.cs index 4e3768299..fac2c498d 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/MotionEventHelper.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/Android/MotionEventHelper.android.cs @@ -6,16 +6,15 @@ namespace Xamarin.CommunityToolkit.UI.Views { class MotionEventHelper { - VisualElement element; + VisualElement? element; bool isInViewCell; - public bool HandleMotionEvent(IViewParent parent, MotionEvent motionEvent) + public bool HandleMotionEvent(IViewParent? parent, MotionEvent? motionEvent) { if (isInViewCell || element == null || motionEvent == null || motionEvent.Action == MotionEventActions.Cancel) return false; - var renderer = parent as VisualElementRenderer; - if (renderer == null || ShouldPassThroughElement()) + if (parent is not VisualElementRenderer renderer || ShouldPassThroughElement()) return false; // Let the container know that we're "fake" handling this event @@ -55,7 +54,7 @@ bool ShouldPassThroughElement() } // This is not a layout and it's transparent; the event can just pass through - if (element.InputTransparent) + if (element?.InputTransparent ?? false) return true; return false; diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/CameraView.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/CameraView.shared.cs index c21a6f21a..06254673c 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/CameraView.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/CameraView.shared.cs @@ -7,13 +7,13 @@ namespace Xamarin.CommunityToolkit.UI.Views { public class CameraView : View { - public event EventHandler MediaCaptured; + public event EventHandler? MediaCaptured; - public event EventHandler MediaCaptureFailed; + public event EventHandler? MediaCaptureFailed; - public event EventHandler OnAvailable; + public event EventHandler? OnAvailable; - internal event EventHandler ShutterClicked; + internal event EventHandler? ShutterClicked; internal static readonly BindablePropertyKey ShutterCommandPropertyKey = BindableProperty.CreateReadOnly(nameof(ShutterCommand), @@ -26,7 +26,7 @@ public class CameraView : View public static readonly BindableProperty ShutterCommandProperty = ShutterCommandPropertyKey.BindableProperty; [Preserve(Conditional = true)] - public ICommand ShutterCommand => (ICommand)GetValue(ShutterCommandProperty); + public ICommand? ShutterCommand => (ICommand?)GetValue(ShutterCommandProperty); public static readonly BindableProperty IsBusyProperty = BindableProperty.Create(nameof(IsBusy), typeof(bool), typeof(CameraView), false); @@ -117,10 +117,12 @@ public double MaxZoom public void Shutter() => ShutterClicked?.Invoke(this, EventArgs.Empty); - static object ShutterCommandValueCreator(BindableObject b) + static object? ShutterCommandValueCreator(BindableObject? b) { - var camera = (CameraView)b; - return new Command(camera.Shutter); + if (b is CameraView camera) + return new Command(camera.Shutter); + + return null; } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/MediaCapturedEventArgs.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/MediaCapturedEventArgs.shared.cs index 10a786a47..1b52ee1ad 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/MediaCapturedEventArgs.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/MediaCapturedEventArgs.shared.cs @@ -7,21 +7,21 @@ namespace Xamarin.CommunityToolkit.UI.Views { public class MediaCapturedEventArgs : EventArgs { - readonly string path; - readonly Lazy imageSource; - readonly Lazy mediaSource; + readonly string? path; + readonly Lazy imageSource; + readonly Lazy mediaSource; internal MediaCapturedEventArgs( - string path = null, - byte[] imageData = null, + string? path = null, + byte[]? imageData = null, double rotation = 0) { // Path = path; this.path = path; Rotation = rotation; ImageData = imageData; - imageSource = new Lazy(GetImageSource); - mediaSource = new Lazy(GetMediaSource); + imageSource = new Lazy(GetImageSource); + mediaSource = new Lazy(GetMediaSource); } // TODO See note on CameraView.SavePhotoToFile. @@ -35,18 +35,18 @@ public class MediaCapturedEventArgs : EventArgs /// /// Raw image data, only filled when taking a picture and SavePhotoToFile is false /// - public byte[] ImageData { get; } + public byte[]? ImageData { get; } /// /// Applied image rotation for correct orientation on Android devices /// public double Rotation { get; } - public ImageSource Image => imageSource.Value; + public ImageSource? Image => imageSource.Value; - public XCT.FileMediaSource Video => mediaSource.Value; + public XCT.FileMediaSource? Video => mediaSource.Value; - ImageSource GetImageSource() + ImageSource? GetImageSource() { if (ImageData != null) return ImageSource.FromStream(() => new MemoryStream(ImageData)); @@ -54,6 +54,12 @@ ImageSource GetImageSource() return !string.IsNullOrEmpty(path) ? path : null; } - XCT.FileMediaSource GetMediaSource() => !string.IsNullOrEmpty(path) ? path : null; + XCT.FileMediaSource? GetMediaSource() + { + if (path != null && !string.IsNullOrEmpty(path)) + return path; + else + return null; + } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/UWP/CameraViewRenderer.uwp.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/UWP/CameraViewRenderer.uwp.cs index 66138156d..da8020c55 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/UWP/CameraViewRenderer.uwp.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/UWP/CameraViewRenderer.uwp.cs @@ -26,21 +26,19 @@ namespace Xamarin.CommunityToolkit.UI.Views { public class CameraViewRenderer : ViewRenderer { - MediaCapture mediaCapture; + readonly MediaEncodingProfile encodingProfile; + + MediaCapture? mediaCapture; bool isPreviewing; - Lamp flash; - LowLagMediaRecording mediaRecording; - string filePath; + Lamp? flash; + LowLagMediaRecording? mediaRecording; + string? filePath; bool busy; - VideoStabilizationEffect videoStabilizationEffect; - MediaEncodingProfile encodingProfile; - VideoEncodingProperties inputPropertiesBackup; - VideoEncodingProperties outputPropertiesBackup; + VideoStabilizationEffect? videoStabilizationEffect; + VideoEncodingProperties? inputPropertiesBackup; + VideoEncodingProperties? outputPropertiesBackup; - public CameraViewRenderer() - { - encodingProfile = MediaEncodingProfile.CreateMp4(VideoEncodingQuality.Auto); - } + public CameraViewRenderer() => encodingProfile = MediaEncodingProfile.CreateMp4(VideoEncodingQuality.Auto); bool IsBusy { @@ -72,13 +70,15 @@ protected override async void OnElementChanged(ElementChangedEventArgs> GetImage() + async Task> GetImage() { + _ = mediaCapture ?? throw new NullReferenceException(); + IsBusy = true; var imageProp = ImageEncodingProperties.CreateUncompressed(MediaPixelFormat.Bgra8); var lowLagCapture = await mediaCapture.PrepareLowLagPhotoCaptureAsync(imageProp); var capturedPhoto = await lowLagCapture.CaptureAsync(); await lowLagCapture.FinishAsync(); - string filePath = null; + string? filePath = null; // See TODO on CameraView.SavePhotoToFile /*if (Element.SavePhotoToFile) @@ -159,24 +161,24 @@ async Task HandleVideo() outputEncoder.SetSoftwareBitmap(capturedPhoto.Frame.SoftwareBitmap); await outputEncoder.FlushAsync(); - byte[] imageData = null; - // See TODO on CameraView.SavePhotoToFile // if (!Element.SavePhotoToFile) // { using var memoryStream = new MemoryStream(); await outputStream.AsStream().CopyToAsync(memoryStream); - imageData = memoryStream.ToArray(); + var imageData = memoryStream.ToArray(); // } IsBusy = false; - return new Tuple(filePath, imageData); + return new Tuple(filePath, imageData); } async Task StartRecord() { + _ = mediaCapture ?? throw new NullReferenceException(); + // TODO replace platform specifics // var localFolder = Element.On().GetVideoFolder(); var localFolder = "Video"; @@ -205,7 +207,7 @@ async Task StartRecord() await mediaRecording.StartAsync(); } - async Task StopRecord() + async Task StopRecord() { if (mediaRecording == null) return null; @@ -214,12 +216,12 @@ async Task StopRecord() await mediaRecording.FinishAsync(); mediaRecording = null; - if (videoStabilizationEffect != null) + if (videoStabilizationEffect != null && mediaCapture != null) { await mediaCapture.RemoveEffectAsync(videoStabilizationEffect); videoStabilizationEffect = null; - if (inputPropertiesBackup != null) + if (inputPropertiesBackup != null && mediaCapture != null) { await mediaCapture.VideoDeviceController.SetMediaStreamPropertiesAsync(MediaStreamType.VideoRecord, inputPropertiesBackup); inputPropertiesBackup = null; @@ -238,7 +240,7 @@ async Task StopRecord() void MediaCaptureFailed(MediaCapture sender, MediaCaptureFailedEventArgs errorEventArgs) => Element?.RaiseMediaCaptureFailed(errorEventArgs.Message); - protected override async void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + protected override async void OnElementPropertyChanged(object? sender, PropertyChangedEventArgs e) { switch (e.PropertyName) { @@ -264,6 +266,7 @@ protected override async void OnElementPropertyChanged(object sender, PropertyCh void UpdateZoom() { + _ = mediaCapture ?? throw new NullReferenceException(); var zoomControl = mediaCapture.VideoDeviceController.ZoomControl; if (!zoomControl.Supported) return; @@ -290,7 +293,7 @@ static float Clamp(double value, float min, float max) } } - DeviceInformation FilterCamera(DeviceInformationCollection cameraDevices, Windows.Devices.Enumeration.Panel panel) + DeviceInformation? FilterCamera(DeviceInformationCollection cameraDevices, Windows.Devices.Enumeration.Panel panel) { foreach (var cam in cameraDevices) { @@ -314,23 +317,15 @@ async Task InitializeCameraAsync() } IsBusy = true; - DeviceInformation device = null; - switch (Element.CameraOptions) + + var device = Element.CameraOptions switch { - default: - case CameraOptions.Default: - device = cameraDevices[0]; - break; - case CameraOptions.Front: - device = FilterCamera(cameraDevices, Windows.Devices.Enumeration.Panel.Front); - break; - case CameraOptions.Back: - device = FilterCamera(cameraDevices, Windows.Devices.Enumeration.Panel.Back); - break; - case CameraOptions.External: - device = FilterCamera(cameraDevices, Windows.Devices.Enumeration.Panel.Unknown); - break; - } + CameraOptions.Front => FilterCamera(cameraDevices, Windows.Devices.Enumeration.Panel.Front), + CameraOptions.Back => FilterCamera(cameraDevices, Windows.Devices.Enumeration.Panel.Back), + CameraOptions.External => FilterCamera(cameraDevices, Windows.Devices.Enumeration.Panel.Unknown), + _ => cameraDevices?[0], + }; + if (device == null) { Element?.RaiseMediaCaptureFailed($"{Element.CameraOptions} camera not found."); @@ -353,8 +348,10 @@ async Task InitializeCameraAsync() AudioDeviceId = selectedAudioDevice }); flash = await Lamp.GetDefaultAsync(); - if (mediaCapture?.VideoDeviceController.ZoomControl.Supported ?? false) + + if (mediaCapture.VideoDeviceController.ZoomControl.Supported) Element.MaxZoom = mediaCapture.VideoDeviceController.ZoomControl.Max; + DisplayInformation.AutoRotationPreferences = DisplayOrientations.Landscape; } catch (UnauthorizedAccessException ex) @@ -415,9 +412,18 @@ protected override async void Dispose(bool disposing) async void CaptureDeviceExclusiveControlStatusChanged(MediaCapture sender, MediaCaptureDeviceExclusiveControlStatusChangedEventArgs args) { if (args.Status == MediaCaptureDeviceExclusiveControlStatus.SharedReadOnlyAvailable) + { Element?.RaiseMediaCaptureFailed("The camera preview can't be displayed because another app has exclusive access"); + } else if (args.Status == MediaCaptureDeviceExclusiveControlStatus.ExclusiveControlAvailable && !isPreviewing) - await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () => await mediaCapture.StartPreviewAsync()); + { + await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () => + { + if (mediaCapture != null) + await mediaCapture.StartPreviewAsync(); + }); + } + IsBusy = false; } } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/iOS/CameraViewRenderer.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/iOS/CameraViewRenderer.ios.cs index 52a930331..3147976de 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/iOS/CameraViewRenderer.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/iOS/CameraViewRenderer.ios.cs @@ -23,6 +23,8 @@ protected override void OnElementChanged(ElementChangedEventArgs e) if (Control == null && !disposed) { SetNativeControl(new FormsCameraView()); + + _ = Control ?? throw new NullReferenceException($"{nameof(Control)} cannot be null"); Control.Busy += OnBusy; Control.Available += OnAvailability; Control.FinishCapture += FinishCapture; @@ -45,15 +47,15 @@ protected override void OnElementChanged(ElementChangedEventArgs e) } } - void OnBusy(object sender, bool busy) => Element.IsBusy = busy; + void OnBusy(object? sender, bool busy) => Element.IsBusy = busy; - void OnAvailability(object sender, bool available) + void OnAvailability(object? sender, bool available) { Element.MaxZoom = Control.MaxZoom; Element.IsAvailable = available; } - void FinishCapture(object sender, Tuple e) + void FinishCapture(object? sender, Tuple e) { if (Element == null || Control == null) return; @@ -70,7 +72,7 @@ void FinishCapture(object sender, Tuple e) // if (!Element.SavePhotoToFile && photoData != null) if (photoData != null) { - var data = UIImage.LoadFromData(photoData).AsJPEG().ToArray(); + var data = UIImage.LoadFromData(photoData)?.AsJPEG().ToArray(); Device.BeginInvokeOnMainThread(() => { Element.RaiseMediaCaptured(new MediaCapturedEventArgs(imageData: data)); @@ -78,7 +80,7 @@ void FinishCapture(object sender, Tuple e) return; } - PHObjectPlaceholder placeholder = null; + PHObjectPlaceholder? placeholder = null; PHPhotoLibrary.RequestAuthorization(status => { if (status != PHAuthorizationStatus.Authorized) @@ -118,7 +120,8 @@ void FinishCapture(object sender, Tuple e) return; } - if (!(PHAsset.FetchAssetsUsingLocalIdentifiers(new[] { placeholder.LocalIdentifier }, null).firstObject is PHAsset asset)) + _ = placeholder ?? throw new NullReferenceException(); + if (PHAsset.FetchAssetsUsingLocalIdentifiers(new[] { placeholder.LocalIdentifier }, null).firstObject is not PHAsset asset) { Element.RaiseMediaCaptureFailed($"Could not save media to photo library"); return; @@ -132,7 +135,7 @@ void FinishCapture(object sender, Tuple e) { Device.BeginInvokeOnMainThread(() => { - Element.RaiseMediaCaptured(new MediaCapturedEventArgs(input.FullSizeImageUrl.Path)); + Element.RaiseMediaCaptured(new MediaCapturedEventArgs(input.FullSizeImageUrl?.Path)); }); }); } @@ -143,7 +146,7 @@ void FinishCapture(object sender, Tuple e) Version = PHVideoRequestOptionsVersion.Original }, (avAsset, mix, info) => { - if (!(avAsset is AVUrlAsset urlAsset)) + if (avAsset is not AVUrlAsset urlAsset) { Element.RaiseMediaCaptureFailed($"Could not save media to photo library"); return; @@ -168,7 +171,7 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + protected override void OnElementPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { base.OnElementPropertyChanged(sender, e); @@ -196,7 +199,7 @@ protected override void OnElementPropertyChanged(object sender, System.Component } } - async void HandleShutter(object sender, EventArgs e) + async void HandleShutter(object? sender, EventArgs e) { switch (Element.CaptureMode) { diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/iOS/FormsCameraView.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/iOS/FormsCameraView.ios.cs index 0f8036389..f460d9184 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/iOS/FormsCameraView.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/CameraView/iOS/FormsCameraView.ios.cs @@ -15,12 +15,12 @@ public sealed class FormsCameraView : UIView, IAVCaptureFileOutputRecordingDeleg readonly AVCaptureVideoPreviewLayer previewLayer; readonly AVCaptureSession captureSession; readonly UIView mainView; - AVCaptureDeviceInput input; - AVCaptureStillImageOutput imageOutput; - AVCapturePhotoOutput photoOutput; - AVCaptureMovieFileOutput videoOutput; - AVCaptureConnection captureConnection; - AVCaptureDevice device; + AVCaptureDeviceInput? input; + AVCaptureStillImageOutput? imageOutput; + AVCapturePhotoOutput? photoOutput; + AVCaptureMovieFileOutput? videoOutput; + AVCaptureConnection? captureConnection; + AVCaptureDevice? device; AVCaptureDevicePosition? lastPosition; bool isBusy; bool isAvailable; @@ -28,11 +28,11 @@ public sealed class FormsCameraView : UIView, IAVCaptureFileOutputRecordingDeleg CameraFlashMode flashMode; readonly float imgScale = 1f; - public event EventHandler Busy; + public event EventHandler? Busy; - public event EventHandler Available; + public event EventHandler? Available; - public event EventHandler> FinishCapture; + public event EventHandler>? FinishCapture; public bool VideoRecorded => videoOutput?.Recording == true; @@ -89,7 +89,7 @@ void SetStartOrientation() } } - void LogError(string message, Exception error = null) + void LogError(string message, Exception? error = null) { var errorMessage = error == null ? string.Empty @@ -116,6 +116,8 @@ bool IsBusy UIImage RotateImage(UIImage image) { var imgRef = image.CGImage; + _ = imgRef ?? throw new NullReferenceException(); + var transform = CGAffineTransform.MakeIdentity(); var imgHeight = imgRef.Height * imgScale; @@ -198,7 +200,7 @@ public async Task TakePhoto() { var photoOutputConnection = photoOutput.ConnectionFromMediaType(AVMediaType.Video); if (photoOutputConnection != null) - photoOutputConnection.VideoOrientation = previewLayer.Connection.VideoOrientation; + photoOutputConnection.VideoOrientation = previewLayer.Connection?.VideoOrientation ?? throw new NullReferenceException(); var photoSettings = AVCapturePhotoSettings.Create(); photoSettings.FlashMode = (AVCaptureFlashMode)flashMode; @@ -208,7 +210,7 @@ public async Task TakePhoto() { OnFinishCapture = (data, error) => { - FinishCapture?.Invoke(this, new Tuple(data, error)); + FinishCapture?.Invoke(this, new Tuple(data, error)); IsBusy = false; }, WillCapturePhotoAnimation = () => Animate(0.25, () => previewLayer.Opacity = 1) @@ -221,17 +223,20 @@ public async Task TakePhoto() // iOS < 10 try { - var connection = imageOutput.Connections[0]; - connection.VideoOrientation = previewLayer.Connection.VideoOrientation; + var connection = imageOutput?.Connections[0] ?? throw new NullReferenceException(); + connection.VideoOrientation = previewLayer.Connection?.VideoOrientation ?? throw new NullReferenceException(); var sampleBuffer = await imageOutput.CaptureStillImageTaskAsync(connection); var imageData = AVCaptureStillImageOutput.JpegStillToNSData(sampleBuffer); - FinishCapture?.Invoke(this, new Tuple(imageData, null)); + FinishCapture?.Invoke(this, new Tuple(imageData, null)); } catch (Exception) { - FinishCapture?.Invoke(this, new Tuple(null, new NSError(new NSString("faled create image"), 0))); + FinishCapture?.Invoke(this, new Tuple(null, new NSError(new NSString("faled create image"), 0))); + } + finally + { + IsBusy = false; } - IsBusy = false; } string ConstructVideoFilename() @@ -268,7 +273,7 @@ public void StartRecord() IsBusy = true; try { - videoOutput.Connections[0].VideoOrientation = previewLayer.Connection.VideoOrientation; + videoOutput.Connections[0].VideoOrientation = previewLayer.Connection?.VideoOrientation ?? throw new NullReferenceException(); var connection = videoOutput.Connections[0]; if (connection.SupportsVideoOrientation) @@ -284,7 +289,10 @@ public void StartRecord() { LogError("Error with camera output capture", error); } - IsBusy = false; + finally + { + IsBusy = false; + } } public void StopRecord() @@ -296,9 +304,12 @@ public void StopRecord() } } - public void FinishedRecording(AVCaptureFileOutput captureOutput, NSUrl outputFileUrl, NSObject[] connections, NSError error) + public void FinishedRecording(AVCaptureFileOutput captureOutput, NSUrl outputFileUrl, NSObject[] connections, NSError? error) { - FinishCapture?.Invoke(this, new Tuple(outputFileUrl, error)); + FinishCapture?.Invoke(this, new Tuple(outputFileUrl, error)); + + _ = videoOutput ?? throw new NullReferenceException(); + captureSession.RemoveOutput(videoOutput); videoOutput = null; IsBusy = false; @@ -316,6 +327,7 @@ void UpdateFlash(CameraFlashMode? newFlashMode = null) { var desiredFlashMode = newFlashMode ?? flashMode; + _ = device ?? throw new NullReferenceException(); device.LockForConfiguration(out var err); if (CheckFlashModeSupported(desiredFlashMode)) @@ -340,10 +352,12 @@ void UpdateFlash(CameraFlashMode? newFlashMode = null) flashMode = desiredFlashMode; } - if (desiredFlashMode != CameraFlashMode.Torch && - device.TorchMode == AVCaptureTorchMode.On && - device.IsTorchModeSupported(AVCaptureTorchMode.Off)) + if (desiredFlashMode != CameraFlashMode.Torch + && device.TorchMode == AVCaptureTorchMode.On + && device.IsTorchModeSupported(AVCaptureTorchMode.Off)) + { device.TorchMode = AVCaptureTorchMode.Off; + } device.UnlockForConfiguration(); } @@ -354,7 +368,10 @@ void UpdateFlash(CameraFlashMode? newFlashMode = null) } bool CheckFlashModeSupported(CameraFlashMode flashMode) - => flashMode switch + { + _ = device ?? throw new NullReferenceException(); + + return flashMode switch { CameraFlashMode.Off => device.IsFlashModeSupported(AVCaptureFlashMode.Off), CameraFlashMode.On => device.IsFlashModeSupported(AVCaptureFlashMode.On), @@ -362,6 +379,7 @@ bool CheckFlashModeSupported(CameraFlashMode flashMode) CameraFlashMode.Torch => device.IsTorchModeSupported(AVCaptureTorchMode.On), _ => device.IsFlashModeSupported(AVCaptureFlashMode.Off) }; + } public bool VideoStabilization { get; set; } @@ -378,6 +396,7 @@ public void ChangeFocusPoint(Point point) try { + _ = device ?? throw new NullReferenceException(); device.LockForConfiguration(out var err); var focus_x = point.X / Bounds.Width; @@ -565,13 +584,13 @@ protected override void Dispose(bool disposing) class PhotoCaptureDelegate : NSObject, IAVCapturePhotoCaptureDelegate { - public Action OnFinishCapture; - public Action WillCapturePhotoAnimation; + public Action? OnFinishCapture; + public Action? WillCapturePhotoAnimation; - NSData photoData; + NSData? photoData; [Export("captureOutput:willCapturePhotoForResolvedSettings:")] - public void WillCapturePhoto(AVCapturePhotoOutput captureOutput, AVCaptureResolvedPhotoSettings resolvedSettings) => WillCapturePhotoAnimation(); + public void WillCapturePhoto(AVCapturePhotoOutput captureOutput, AVCaptureResolvedPhotoSettings resolvedSettings) => WillCapturePhotoAnimation?.Invoke(); [Export("captureOutput:didFinishProcessingPhotoSampleBuffer:previewPhotoSampleBuffer:resolvedSettings:bracketSettings:error:")] public void DidFinishProcessingPhoto(AVCapturePhotoOutput captureOutput, CMSampleBuffer photoSampleBuffer, CMSampleBuffer previewPhotoSampleBuffer, AVCaptureResolvedPhotoSettings resolvedSettings, AVCaptureBracketedStillImageSettings bracketSettings, NSError error) @@ -584,6 +603,6 @@ public void DidFinishProcessingPhoto(AVCapturePhotoOutput captureOutput, CMSampl [Export("captureOutput:didFinishCaptureForResolvedSettings:error:")] public void DidFinishCapture(AVCapturePhotoOutput captureOutput, AVCaptureResolvedPhotoSettings resolvedSettings, NSError error) - => OnFinishCapture(photoData, error); + => OnFinishCapture?.Invoke(photoData, error); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Expander/Expander.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Expander/Expander.shared.cs index fdfe53f74..18b54a4af 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Expander/Expander.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Expander/Expander.shared.cs @@ -22,11 +22,11 @@ public class Expander : BaseTemplatedView remove => tappedEventManager.RemoveEventHandler(value); } - ContentView contentHolder; + ContentView? contentHolder; - GestureRecognizer headerTapGestureRecognizer; + GestureRecognizer? headerTapGestureRecognizer; - DataTemplate previousTemplate; + DataTemplate? previousTemplate; double lastVisibleSize = -1; @@ -80,8 +80,8 @@ public class Expander : BaseTemplatedView : Width; double ContentSize => Direction.IsVertical() - ? contentHolder.Height - : contentHolder.Width; + ? contentHolder?.Height ?? throw new NullReferenceException() + : contentHolder?.Width ?? throw new NullReferenceException(); double ContentSizeRequest { @@ -91,7 +91,7 @@ public class Expander : BaseTemplatedView ? Content.HeightRequest : Content.WidthRequest; - if (sizeRequest < 0 || !(Content is Layout layout)) + if (sizeRequest < 0 || Content is not Layout layout) return sizeRequest; return sizeRequest + (Direction.IsVertical() @@ -101,6 +101,8 @@ public class Expander : BaseTemplatedView set { + _ = contentHolder ?? throw new NullReferenceException(); + if (Direction.IsVertical()) { contentHolder.HeightRequest = value; @@ -111,12 +113,12 @@ public class Expander : BaseTemplatedView } double MeasuredContentSize => Direction.IsVertical() - ? contentHolder.Measure(Width, double.PositiveInfinity).Request.Height - : contentHolder.Measure(double.PositiveInfinity, Height).Request.Width; + ? contentHolder?.Measure(Width, double.PositiveInfinity).Request.Height ?? throw new NullReferenceException() + : contentHolder?.Measure(double.PositiveInfinity, Height).Request.Width ?? throw new NullReferenceException(); - public View Header + public View? Header { - get => (View)GetValue(HeaderProperty); + get => (View?)GetValue(HeaderProperty); set => SetValue(HeaderProperty, value); } @@ -126,9 +128,9 @@ public View Content set => SetValue(ContentProperty, value); } - public DataTemplate ContentTemplate + public DataTemplate? ContentTemplate { - get => (DataTemplate)GetValue(ContentTemplateProperty); + get => (DataTemplate?)GetValue(ContentTemplateProperty); set => SetValue(ContentTemplateProperty, value); } @@ -174,15 +176,15 @@ public ExpandState State set => SetValue(StateProperty, value); } - public object CommandParameter + public object? CommandParameter { get => GetValue(CommandParameterProperty); set => SetValue(CommandParameterProperty, value); } - public ICommand Command + public ICommand? Command { - get => (ICommand)GetValue(CommandProperty); + get => (ICommand?)GetValue(CommandProperty); set => SetValue(CommandProperty, value); } @@ -206,7 +208,7 @@ protected override void OnControlInitialized(StackLayout control) CommandParameter = this, Command = new Command(parameter => { - var parent = (parameter as View).Parent; + var parent = ((View)parameter).Parent; while (parent != null && !(parent is Page)) { if (parent is Expander ancestorExpander) @@ -380,7 +382,7 @@ void SetContent() SetContent(true); } - View CreateContent() + View? CreateContent() { var template = ContentTemplate; while (template is DataTemplateSelector selector) @@ -390,11 +392,13 @@ View CreateContent() return null; previousTemplate = template; - return (View)template?.CreateContent(); + return (View?)template?.CreateContent(); } void SetDirection(ExpandDirection oldDirection) { + _ = Header ?? throw new InvalidOperationException($"{nameof(Header)} not initialized"); + if (oldDirection.IsVertical() == Direction.IsVertical()) { SetHeader(Header); @@ -422,6 +426,8 @@ void InvokeAnimation(double startSize, double endSize, bool shouldIgnoreAnimatio ? ExpandState.Expanded : ExpandState.Collapsed; ContentSizeRequest = endSize; + + _ = contentHolder ?? throw new NullReferenceException(); contentHolder.IsVisible = IsExpanded; return; } @@ -447,6 +453,7 @@ void InvokeAnimation(double startSize, double endSize, bool shouldIgnoreAnimatio if (!IsExpanded) { + _ = contentHolder ?? throw new NullReferenceException(); contentHolder.IsVisible = false; State = ExpandState.Collapsed; return; diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/Android/GravatarImageSourceHandler.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/Android/GravatarImageSourceHandler.android.cs index 356937c6c..1ade839bc 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/Android/GravatarImageSourceHandler.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/Android/GravatarImageSourceHandler.android.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; using Android.Content; using Android.Graphics; @@ -10,11 +11,11 @@ namespace Xamarin.CommunityToolkit.UI.Views { public partial class GravatarImageSourceHandler : IImageSourceHandler { - public async Task LoadImageAsync(ImageSource imagesource, Context context, CancellationToken cancelationToken = default) + public async Task LoadImageAsync(ImageSource imagesource, Context context, CancellationToken cancelationToken = default) { - var fileInfo = await LoadInternal(imagesource, 1, Application.Context.CacheDir.AbsolutePath); + var fileInfo = await LoadInternal(imagesource, 1, Application.Context.CacheDir?.AbsolutePath ?? throw new NullReferenceException()); - Bitmap bitmap = null; + Bitmap? bitmap = null; try { await semaphore.WaitAsync(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/GravatarImageSource.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/GravatarImageSource.shared.cs index 0c654201d..8ae707d4f 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/GravatarImageSource.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/GravatarImageSource.shared.cs @@ -26,9 +26,9 @@ static void OnGravatarPropertyChanged(BindableObject bindable, object oldValue, gis.OnSourceChanged(); } - public string Email + public string? Email { - get => (string)GetValue(EmailProperty); + get => (string?)GetValue(EmailProperty); set => SetValue(EmailProperty, value); } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/MacOS/GravatarImageSourceHandler.macos.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/MacOS/GravatarImageSourceHandler.macos.cs index 330b57075..e77db2578 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/MacOS/GravatarImageSourceHandler.macos.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/MacOS/GravatarImageSourceHandler.macos.cs @@ -8,11 +8,11 @@ namespace Xamarin.CommunityToolkit.UI.Views { public partial class GravatarImageSourceHandler : IImageSourceHandler { - public async Task LoadImageAsync(ImageSource imagesource, CancellationToken cancelationToken = default, float scale = 1) + public async Task LoadImageAsync(ImageSource imagesource, CancellationToken cancelationToken = default, float scale = 1) { var fileInfo = await LoadInternal(imagesource, 1, GetCacheDirectory()); - NSImage image = null; + NSImage? image = null; try { await semaphore.WaitAsync(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/Shared/GravatarImageSourceHandler.android.ios.macos.tizen.uwp.wpf.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/Shared/GravatarImageSourceHandler.android.ios.macos.tizen.uwp.wpf.cs index 383c3b587..473c8e182 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/Shared/GravatarImageSourceHandler.android.ios.macos.tizen.uwp.wpf.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/Shared/GravatarImageSourceHandler.android.ios.macos.tizen.uwp.wpf.cs @@ -15,7 +15,7 @@ public partial class GravatarImageSourceHandler static readonly Lazy lazyHttp = new Lazy(() => new HttpClient()); static readonly SemaphoreSlim semaphore = new SemaphoreSlim(1); - public static async Task LoadInternal(ImageSource imageSource, float scale, string cacheDirectory) + public static async Task LoadInternal(ImageSource imageSource, float scale, string cacheDirectory) { if (imageSource is GravatarImageSource gis) { @@ -24,6 +24,7 @@ public static async Task LoadInternal(ImageSource imageSource, float s if (!await UseCacheFile(gis.CachingEnabled, gis.CacheValidity, cacheFileInfo)) { + _ = gis.Email ?? throw new InvalidOperationException($"{nameof(gis.Email)} is not initialized"); var imageBytes = await GetGravatarAsync(gis.Email, gis.Size, scale, gis.Default); await SaveImage(cacheFileInfo, imageBytes ?? Array.Empty()); } @@ -78,7 +79,10 @@ static async Task UseCacheFile(bool cachingEnabled, TimeSpan cacheValidity } static string CacheFileName(GravatarImageSource gis, float scale) - => $"{GetMd5Hash(gis.Email)}-{gis.Size}@{scale}x.png"; + { + _ = gis.Email ?? throw new InvalidOperationException($"{nameof(gis.Email)} cannot be null"); + return $"{GetMd5Hash(gis.Email)}-{gis.Size}@{scale}x.png"; + } static async Task GetGravatarAsync(string email, int size, float scale, DefaultGravatar defaultGravatar) { diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/Shared/GravatarImageSourceHandler.ios.macos.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/Shared/GravatarImageSourceHandler.ios.macos.cs index fc80a2131..a7b633e45 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/Shared/GravatarImageSourceHandler.ios.macos.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/Shared/GravatarImageSourceHandler.ios.macos.cs @@ -1,4 +1,5 @@ -using Foundation; +using System; +using Foundation; namespace Xamarin.CommunityToolkit.UI.Views { @@ -8,10 +9,8 @@ static string GetCacheDirectory() { var dirs = NSSearchPath.GetDirectories(NSSearchPathDirectory.CachesDirectory, NSSearchPathDomain.User); if (dirs == null || dirs.Length == 0) - { - // this should never happen... - return null; - } + throw new NotSupportedException(); + return dirs[0]; } } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/UWP/GravatarImageSourceHandler.uwp.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/UWP/GravatarImageSourceHandler.uwp.cs index 6a9301a74..f291022ac 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/UWP/GravatarImageSourceHandler.uwp.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/UWP/GravatarImageSourceHandler.uwp.cs @@ -12,11 +12,11 @@ namespace Xamarin.CommunityToolkit.UI.Views { public partial class GravatarImageSourceHandler : IImageSourceHandler { - public async Task LoadImageAsync(FormsImageSource imagesource, CancellationToken cancellationToken = default) + public async Task LoadImageAsync(FormsImageSource imagesource, CancellationToken cancellationToken = default) { var fileInfo = await LoadInternal(imagesource, 1, ApplicationData.Current.LocalCacheFolder.Path); - BitmapImage bitmap = null; + BitmapImage? bitmap = null; try { await semaphore.WaitAsync(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/Wpf/GravatarImageSourceHandler.wpf.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/Wpf/GravatarImageSourceHandler.wpf.cs index d2b78f88e..cd50af624 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/Wpf/GravatarImageSourceHandler.wpf.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/Wpf/GravatarImageSourceHandler.wpf.cs @@ -12,12 +12,12 @@ namespace Xamarin.CommunityToolkit.UI.Views { public partial class GravatarImageSourceHandler : IImageSourceHandler { - public async Task LoadImageAsync(FormsImageSource imagesource, CancellationToken cancellationToken = default) + public async Task LoadImageAsync(FormsImageSource imagesource, CancellationToken cancellationToken = default) { var appdata = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); var fileInfo = await LoadInternal(imagesource, 1, appdata); - BitmapImage bitmap = null; + BitmapImage? bitmap = null; try { await semaphore.WaitAsync(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/iOS/GravatarImageSourceHandler.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/iOS/GravatarImageSourceHandler.ios.cs index 1f0d06ddc..020ba5f60 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/iOS/GravatarImageSourceHandler.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/GravatarImageSource/iOS/GravatarImageSourceHandler.ios.cs @@ -8,11 +8,11 @@ namespace Xamarin.CommunityToolkit.UI.Views { public partial class GravatarImageSourceHandler : IImageSourceHandler { - public async Task LoadImageAsync(ImageSource imagesource, CancellationToken cancelationToken = default, float scale = 1) + public async Task LoadImageAsync(ImageSource imagesource, CancellationToken cancelationToken = default, float scale = 1) { var fileInfo = await LoadInternal(imagesource, scale, GetCacheDirectory()); - UIImage image = null; + UIImage? image = null; try { await semaphore.WaitAsync(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/LazyView/BaseLazyView.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/LazyView/BaseLazyView.shared.cs new file mode 100644 index 000000000..4fe6373d8 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/LazyView/BaseLazyView.shared.cs @@ -0,0 +1,50 @@ +using System; +using System.Threading.Tasks; +using Xamarin.Forms; +using Xamarin.Forms.Internals; + +namespace Xamarin.CommunityToolkit.UI.Views +{ + [Preserve(Conditional = true)] + public abstract class BaseLazyView : ContentView, IDisposable + { + internal static readonly BindablePropertyKey IsLoadedPropertyKey = BindableProperty.CreateReadOnly(nameof(IsLoaded), typeof(bool), typeof(BaseLazyView), default); + + /// + /// This is a read-only that indicates when the view is loaded. + /// + public static readonly BindableProperty IsLoadedProperty = IsLoadedPropertyKey.BindableProperty; + + /// + /// This is a read-only property that indicates when the view is loaded. + /// + public bool IsLoaded => (bool)GetValue(IsLoadedProperty); + + /// + /// This method change the value of the property. + /// + /// + protected void SetIsLoaded(bool isLoaded) => SetValue(IsLoadedPropertyKey, isLoaded); + + /// + /// Use this method to do the initialization of the and change the status IsLoaded value here. + /// + /// + public abstract ValueTask LoadViewAsync(); + + /// + /// This method dispose the if it's . + /// + public void Dispose() + { + if (Content is IDisposable disposable) + disposable.Dispose(); + } + + protected override void OnBindingContextChanged() + { + if (Content != null && !(Content is ActivityIndicator)) + Content.BindingContext = BindingContext; + } + } +} \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/LazyView/LazyView.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/LazyView/LazyView.shared.cs new file mode 100644 index 000000000..7ea9df102 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/LazyView/LazyView.shared.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.UI.Views +{ + /// + /// This a basic implementation of the LazyView based on use this an exemple to create yours + /// + /// Any + public class LazyView : BaseLazyView where TView : View, new() + { + /// + /// This method initialize your . + /// + /// + public override ValueTask LoadViewAsync() + { + View view = new TView { BindingContext = BindingContext }; + + Content = view; + + SetIsLoaded(true); + return new ValueTask(Task.FromResult(true)); + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/Android/FormsVideoView.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/Android/FormsVideoView.android.cs index 4707a51a4..8c2c1711c 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/Android/FormsVideoView.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/Android/FormsVideoView.android.cs @@ -9,7 +9,7 @@ namespace Xamarin.CommunityToolkit.UI.Views { public class FormsVideoView : VideoView { - public event EventHandler MetadataRetrieved; + public event EventHandler? MetadataRetrieved; public FormsVideoView(Context context) : base(context) @@ -17,7 +17,7 @@ public FormsVideoView(Context context) SetBackgroundColor(global::Android.Graphics.Color.Transparent); } - public override void SetVideoPath(string path) + public override async void SetVideoPath(string? path) { base.SetVideoPath(path); @@ -25,7 +25,7 @@ public override void SetVideoPath(string path) { var retriever = new MediaMetadataRetriever(); - Task.Run(() => + await Task.Run(() => { retriever.SetDataSource(path); ExtractMetadata(retriever); @@ -50,31 +50,30 @@ protected void ExtractMetadata(MediaMetadataRetriever retriever) DurationTimeSpan = null; } - public override void SetVideoURI(global::Android.Net.Uri uri, IDictionary headers) + public override async void SetVideoURI(global::Android.Net.Uri? uri, IDictionary? headers) { - GetMetaData(uri, headers); + if (uri != null) + await SetMetadata(uri, headers); + base.SetVideoURI(uri, headers); } - protected void GetMetaData(global::Android.Net.Uri uri, IDictionary headers) + protected async Task SetMetadata(global::Android.Net.Uri uri, IDictionary? headers) { - Task.Run(() => - { - var retriever = new MediaMetadataRetriever(); + var retriever = new MediaMetadataRetriever(); - if (uri.Scheme != null && uri.Scheme.StartsWith("http")) - { - retriever.SetDataSource(uri.ToString(), headers ?? new Dictionary()); - } - else - { - retriever.SetDataSource(Context, uri); - } + if (uri.Scheme != null && uri.Scheme.StartsWith(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)) + { + await retriever.SetDataSourceAsync(uri.ToString(), headers ?? new Dictionary()); + } + else + { + await retriever.SetDataSourceAsync(Context, uri); + } - ExtractMetadata(retriever); + ExtractMetadata(retriever); - MetadataRetrieved?.Invoke(this, EventArgs.Empty); - }); + MetadataRetrieved?.Invoke(this, EventArgs.Empty); } public int VideoHeight { get; private set; } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/Android/MediaElementRenderer.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/Android/MediaElementRenderer.android.cs index b5ef6c0cb..9ff8a1618 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/Android/MediaElementRenderer.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/Android/MediaElementRenderer.android.cs @@ -17,9 +17,9 @@ namespace Xamarin.CommunityToolkit.UI.Views { public class MediaElementRenderer : FrameLayout, IVisualElementRenderer, IViewRenderer, MediaPlayer.IOnCompletionListener, MediaPlayer.IOnInfoListener, MediaPlayer.IOnPreparedListener, MediaPlayer.IOnErrorListener { - VisualElementTracker tracker; - protected MediaController controller; - protected MediaPlayer mediaPlayer; + VisualElementTracker? tracker; + protected MediaController? controller; + protected MediaPlayer? mediaPlayer; protected FormsVideoView view; bool isDisposed; int? defaultLabelFor; @@ -27,7 +27,7 @@ public class MediaElementRenderer : FrameLayout, IVisualElementRenderer, IViewRe public MediaElementRenderer(Context context) : base(context) { - view = new FormsVideoView(Context); + view = new FormsVideoView(context); view.SetZOrderMediaOverlay(true); view.SetOnCompletionListener(this); view.SetOnInfoListener(this); @@ -47,30 +47,31 @@ public MediaElementRenderer(Context context) public override float Alpha { get => view.Alpha; - set => - + set + { // VideoView opens a separate Window above the current one. // This is because it is based on the SurfaceView. // And we cannot set alpha or perform animations with it because it is not synchronized with your other UI elements. // We may set 0 or 1 alpha only. view.Alpha = Math.Sign(Math.Abs(value)); + } } - protected ToolKitMediaElement MediaElement { get; set; } + protected ToolKitMediaElement? MediaElement { get; set; } - IMediaElementController Controller => MediaElement; + IMediaElementController? Controller => MediaElement; - public VisualElement Element => MediaElement; + public VisualElement? Element => MediaElement; - VisualElementTracker IVisualElementRenderer.Tracker => tracker; + VisualElementTracker? IVisualElementRenderer.Tracker => tracker; - ViewGroup IVisualElementRenderer.ViewGroup => null; + ViewGroup? IVisualElementRenderer.ViewGroup => null; AView IVisualElementRenderer.View => this; - public event EventHandler ElementChanged; + public event EventHandler? ElementChanged; - public event EventHandler ElementPropertyChanged; + public event EventHandler? ElementPropertyChanged; SizeRequest IVisualElementRenderer.GetDesiredSize(int widthConstraint, int heightConstraint) { @@ -125,14 +126,15 @@ void IVisualElementRenderer.SetElement(VisualElement element) if (tracker == null) SetTracker(new VisualElementTracker(this)); - OnElementChanged(new ElementChangedEventArgs(oldElement, MediaElement)); + OnElementChanged(new ElementChangedEventArgs(oldElement, MediaElement)); } - void StateRequested(object sender, StateRequested e) + void StateRequested(object? sender, StateRequested e) { if (view == null) return; + _ = Controller ?? throw new NullReferenceException(); switch (e.State) { case MediaElementState.Playing: @@ -159,17 +161,17 @@ void StateRequested(object sender, StateRequested e) Controller.Position = view.Position; } - void OnPositionRequested(object sender, EventArgs e) + void OnPositionRequested(object? sender, EventArgs e) { - if (view == null) + if (view == null || Controller == null) return; Controller.Position = view.Position; } - void SeekRequested(object sender, SeekRequested e) + void SeekRequested(object? sender, SeekRequested e) { - if (view == null) + if (view == null || Controller == null) return; view.SeekTo((int)e.Position.TotalMilliseconds); @@ -187,7 +189,11 @@ void IVisualElementRenderer.SetLabelFor(int? id) void SetTracker(VisualElementTracker tracker) => this.tracker = tracker; - protected virtual void UpdateBackgroundColor() => SetBackgroundColor(Element.BackgroundColor.ToAndroid()); + protected virtual void UpdateBackgroundColor() + { + if (Element != null) + SetBackgroundColor(Element.BackgroundColor.ToAndroid()); + } void IVisualElementRenderer.UpdateLayout() => tracker?.UpdateLayout(); @@ -208,13 +214,13 @@ protected override void Dispose(bool disposing) tracker?.Dispose(); if (Element != null) - UnsubscribeFromEvents(Element as MediaElement); + UnsubscribeFromEvents((ToolKitMediaElement)Element); } base.Dispose(disposing); } - protected virtual void OnElementChanged(ElementChangedEventArgs e) + protected virtual void OnElementChanged(ElementChangedEventArgs e) { if (e.NewElement != null) { @@ -232,9 +238,9 @@ protected virtual void OnElementChanged(ElementChangedEventArgs e) ElementChanged?.Invoke(this, new VisualElementChangedEventArgs(e.OldElement, e.NewElement)); } - void MetadataRetrieved(object sender, EventArgs e) + void MetadataRetrieved(object? sender, EventArgs e) { - if (view == null) + if (view == null || Controller == null) return; Controller.Duration = view.DurationTimeSpan; @@ -244,7 +250,7 @@ void MetadataRetrieved(object sender, EventArgs e) Device.BeginInvokeOnMainThread(UpdateLayoutParameters); } - protected virtual void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) + protected virtual void OnElementPropertyChanged(object? sender, PropertyChangedEventArgs e) { switch (e.PropertyName) { @@ -253,7 +259,7 @@ protected virtual void OnElementPropertyChanged(object sender, PropertyChangedEv break; case nameof(MediaElement.IsLooping): - if (mediaPlayer != null) + if (mediaPlayer != null && MediaElement != null) mediaPlayer.Looping = MediaElement.IsLooping; break; @@ -279,7 +285,7 @@ protected virtual void OnElementPropertyChanged(object sender, PropertyChangedEv protected virtual void UpdateKeepScreenOn() { - if (view == null) + if (view == null || MediaElement == null) return; view.KeepScreenOn = MediaElement.KeepScreenOn; @@ -287,7 +293,7 @@ protected virtual void UpdateKeepScreenOn() protected void UpdateShowPlaybackControls() { - if (controller == null) + if (controller == null || MediaElement == null) return; controller.Visibility = MediaElement.ShowsPlaybackControls ? ViewStates.Visible : ViewStates.Gone; @@ -298,21 +304,21 @@ protected virtual void UpdateSource() if (view == null) return; - if (MediaElement.Source != null) + if (MediaElement?.Source != null) { if (MediaElement.Source is XCT.UriMediaSource uriSource) { - if (uriSource.Uri.Scheme == "ms-appx") + if (uriSource.Uri?.Scheme is "ms-appx") { if (uriSource.Uri.LocalPath.Length <= 1) return; // Video resources should be in the raw folder with Build Action set to AndroidResource - var uri = "android.resource://" + Context.PackageName + "/raw/" + + var uri = "android.resource://" + Context?.PackageName + "/raw/" + uriSource.Uri.LocalPath[1..uriSource.Uri.LocalPath.LastIndexOf('.')].ToLower(); view.SetVideoURI(global::Android.Net.Uri.Parse(uri)); } - else if (uriSource.Uri.Scheme == "ms-appdata") + else if (uriSource.Uri?.Scheme is "ms-appdata") { var filePath = ResolveMsAppDataUri(uriSource.Uri); @@ -321,21 +327,29 @@ protected virtual void UpdateSource() view.SetVideoPath(filePath); } - else + else if (uriSource.Uri != null) { - if (uriSource.Uri.IsFile) + if (uriSource.Uri.IsFile is true) view.SetVideoPath(uriSource.Uri.AbsolutePath); else view.SetVideoURI(global::Android.Net.Uri.Parse(uriSource.Uri.ToString())); } + else + { + throw new InvalidOperationException($"{nameof(uriSource.Uri)} not initialized"); + } } else if (MediaElement.Source is XCT.FileMediaSource fileSource) + { view.SetVideoPath(fileSource.File); + } if (MediaElement.AutoPlay) { view.Start(); - Controller.CurrentState = view.IsPlaying ? MediaElementState.Playing : MediaElementState.Stopped; + + if (Controller != null) + Controller.CurrentState = view.IsPlaying ? MediaElementState.Playing : MediaElementState.Stopped; } } else @@ -345,13 +359,15 @@ protected virtual void UpdateSource() view.SetVideoURI(null); view.Visibility = ViewStates.Gone; view.Visibility = ViewStates.Visible; - Controller.CurrentState = MediaElementState.Stopped; + + if (Controller != null) + Controller.CurrentState = MediaElementState.Stopped; } } protected void UpdateVolume() { - if (view == null) + if (MediaElement == null) return; mediaPlayer?.SetVolume((float)MediaElement.Volume, (float)MediaElement.Volume); @@ -359,7 +375,7 @@ protected void UpdateVolume() protected string ResolveMsAppDataUri(Uri uri) { - if (uri.Scheme == "ms-appdata") + if (uri.Scheme is "ms-appdata") { string filePath; if (uri.LocalPath.StartsWith("/local")) @@ -375,18 +391,18 @@ protected string ResolveMsAppDataUri(Uri uri) throw new ArgumentException("uri"); } - void MediaPlayer.IOnCompletionListener.OnCompletion(MediaPlayer mp) + void MediaPlayer.IOnCompletionListener.OnCompletion(MediaPlayer? mp) { - if (Controller == null) + if (Controller == null || mediaPlayer == null) return; Controller.Position = TimeSpan.FromMilliseconds(mediaPlayer.CurrentPosition); Controller.OnMediaEnded(); } - void MediaPlayer.IOnPreparedListener.OnPrepared(MediaPlayer mp) + void MediaPlayer.IOnPreparedListener.OnPrepared(MediaPlayer? mp) { - if (Controller == null) + if (Controller == null || mp == null || MediaElement == null) return; Controller.OnMediaOpened(); @@ -421,7 +437,7 @@ protected virtual void UpdateLayoutParameters() var ratio = view.VideoWidth / (float)view.VideoHeight; var controlRatio = (float)Width / Height; - switch (MediaElement.Aspect) + switch (MediaElement?.Aspect) { case Aspect.Fill: // TODO: This doesn't stretch like other platforms... @@ -472,7 +488,6 @@ protected virtual void ReleaseControl() view.SetOnPreparedListener(null); view.SetOnCompletionListener(null); view.Dispose(); - view = null; } if (controller != null) @@ -488,7 +503,7 @@ protected virtual void ReleaseControl() } } - bool MediaPlayer.IOnErrorListener.OnError(MediaPlayer mp, MediaError what, int extra) + bool MediaPlayer.IOnErrorListener.OnError(MediaPlayer? mp, MediaError what, int extra) { if (Controller == null) return false; @@ -497,9 +512,9 @@ bool MediaPlayer.IOnErrorListener.OnError(MediaPlayer mp, MediaError what, int e return false; } - bool MediaPlayer.IOnInfoListener.OnInfo(MediaPlayer mp, MediaInfo what, int extra) + bool MediaPlayer.IOnInfoListener.OnInfo(MediaPlayer? mp, MediaInfo what, int extra) { - if (view == null) + if (view == null || mp == null || Controller == null) return false; switch (what) @@ -525,6 +540,10 @@ bool MediaPlayer.IOnInfoListener.OnInfo(MediaPlayer mp, MediaInfo what, int extr return true; } - void OnMpBufferingUpdate(object sender, MediaPlayer.BufferingUpdateEventArgs e) => Controller.BufferingProgress = e.Percent / 100f; + void OnMpBufferingUpdate(object? sender, MediaPlayer.BufferingUpdateEventArgs e) + { + if (Controller != null) + Controller.BufferingProgress = e.Percent / 100f; + } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/MediaElement.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/MediaElement.shared.cs index 136dd8b0e..558a3a510 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/MediaElement.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/MediaElement.shared.cs @@ -102,9 +102,9 @@ public TimeSpan Position } [Forms.TypeConverter(typeof(MediaSourceConverter))] - public MediaSource Source + public MediaSource? Source { - get => (MediaSource)GetValue(SourceProperty); + get => (MediaSource?)GetValue(SourceProperty); set => SetValue(SourceProperty, value); } @@ -118,19 +118,19 @@ public double Volume set => SetValue(VolumeProperty, value); } - internal event EventHandler SeekRequested; + internal event EventHandler? SeekRequested; - internal event EventHandler StateRequested; + internal event EventHandler? StateRequested; - internal event EventHandler PositionRequested; + internal event EventHandler? PositionRequested; - public event EventHandler MediaEnded; + public event EventHandler? MediaEnded; - public event EventHandler MediaFailed; + public event EventHandler? MediaFailed; - public event EventHandler MediaOpened; + public event EventHandler? MediaOpened; - public event EventHandler SeekCompleted; + public event EventHandler? SeekCompleted; public void Play() => StateRequested?.Invoke(this, new StateRequested(MediaElementState.Playing)); @@ -212,7 +212,7 @@ protected override void OnBindingContextChanged() base.OnBindingContextChanged(); } - void OnSourceChanged(object sender, EventArgs eventArgs) + void OnSourceChanged(object? sender, EventArgs eventArgs) { OnPropertyChanged(SourceProperty.PropertyName); InvalidateMeasure(); @@ -245,7 +245,7 @@ void OnSourcePropertyChanging(MediaSource oldvalue) static void CurrentStateChanged(BindableObject bindable, object oldValue, object newValue) { - var element = bindable as MediaElement; + var element = (MediaElement)bindable; switch ((MediaElementState)newValue) { @@ -269,7 +269,7 @@ static void CurrentStateChanged(BindableObject bindable, object oldValue, object static void PositionChanged(BindableObject bindable, object oldValue, object newValue) { - var element = bindable as MediaElement; + var element = (MediaElement)bindable; var oldval = (TimeSpan)oldValue; var newval = (TimeSpan)newValue; diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/UWP/MediaElementRenderer.uwp.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/UWP/MediaElementRenderer.uwp.cs index 00f6e71bd..ea260a89b 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/UWP/MediaElementRenderer.uwp.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/UWP/MediaElementRenderer.uwp.cs @@ -86,7 +86,7 @@ protected override void OnElementChanged(ElementChangedEventArgs Element as IMediaElementController; - void StateRequested(object sender, StateRequested e) + void StateRequested(object? sender, StateRequested e) { if (Control != null) { @@ -118,7 +118,7 @@ void StateRequested(object sender, StateRequested e) } } - void SeekRequested(object sender, SeekRequested e) + void SeekRequested(object? sender, SeekRequested e) { if (Control != null && Control.CanSeek) { @@ -127,9 +127,9 @@ void SeekRequested(object sender, SeekRequested e) } } - void ControlMediaFailed(object sender, ExceptionRoutedEventArgs e) => Controller?.OnMediaFailed(); + void ControlMediaFailed(object? sender, ExceptionRoutedEventArgs e) => Controller?.OnMediaFailed(); - void ControlMediaEnded(object sender, RoutedEventArgs e) + void ControlMediaEnded(object? sender, RoutedEventArgs e) { if (Control != null) Controller.Position = Control.Position; @@ -138,7 +138,7 @@ void ControlMediaEnded(object sender, RoutedEventArgs e) Controller.OnMediaEnded(); } - void ControlMediaOpened(object sender, RoutedEventArgs e) + void ControlMediaOpened(object? sender, RoutedEventArgs e) { Controller.Duration = Control.NaturalDuration.HasTimeSpan ? Control.NaturalDuration.TimeSpan : (TimeSpan?)null; Controller.VideoHeight = Control.NaturalVideoHeight; @@ -149,9 +149,9 @@ void ControlMediaOpened(object sender, RoutedEventArgs e) Controller.OnMediaOpened(); } - void ControlCurrentStateChanged(object sender, RoutedEventArgs e) + void ControlCurrentStateChanged(object? sender, RoutedEventArgs e) { - if (Element is null || Control is null) + if (Element == null || Control == null) return; switch (Control.CurrentState) @@ -195,7 +195,7 @@ void PositionChanged(DependencyObject sender, DependencyProperty dp) Controller.Position = Control.Position; } - void ControlSeekCompleted(object sender, RoutedEventArgs e) + void ControlSeekCompleted(object? sender, RoutedEventArgs e) { if (Control != null) { @@ -204,7 +204,7 @@ void ControlSeekCompleted(object sender, RoutedEventArgs e) } } - protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + protected override void OnElementPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { switch (e.PropertyName) { diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/UriTypeConverter.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/UriTypeConverter.shared.cs index 49fc834a0..b82bbc410 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/UriTypeConverter.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/UriTypeConverter.shared.cs @@ -7,7 +7,7 @@ namespace Xamarin.CommunityToolkit.UI.Views [TypeConversion(typeof(Uri))] public class UriTypeConverter : TypeConverter { - public override object ConvertFromInvariantString(string value) => - string.IsNullOrWhiteSpace(value) ? null : (object)new Uri(value, UriKind.RelativeOrAbsolute); + public override object? ConvertFromInvariantString(string? value) => + string.IsNullOrWhiteSpace(value) ? null : new Uri(value, UriKind.RelativeOrAbsolute); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/iOS/MediaElementRenderer.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/iOS/MediaElementRenderer.ios.cs index 35aab8e8d..bc4622edc 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/iOS/MediaElementRenderer.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/MediaElement/iOS/MediaElementRenderer.ios.cs @@ -21,12 +21,12 @@ public class MediaElementRenderer : ViewRenderer IMediaElementController Controller => Element; protected readonly AVPlayerViewController avPlayerViewController = new AVPlayerViewController(); - protected NSObject playedToEndObserver; - protected IDisposable statusObserver; - protected IDisposable rateObserver; - protected IDisposable volumeObserver; + protected NSObject? playedToEndObserver; + protected IDisposable? statusObserver; + protected IDisposable? rateObserver; + protected IDisposable? volumeObserver; bool idleTimerDisabled = false; - AVPlayerItem playerItem; + AVPlayerItem? playerItem; public MediaElementRenderer() => AddPlayedToEndObserver(); @@ -51,11 +51,11 @@ protected virtual void UpdateSource() { if (Element.Source != null) { - AVAsset asset = null; + AVAsset? asset = null; if (Element.Source is XCT.UriMediaSource uriSource) { - if (uriSource.Uri.Scheme == "ms-appx") + if (uriSource.Uri?.Scheme is "ms-appx") { if (uriSource.Uri.LocalPath.Length <= 1) return; @@ -63,7 +63,7 @@ protected virtual void UpdateSource() // used for a file embedded in the application package asset = AVAsset.FromUrl(NSUrl.FromFilename(uriSource.Uri.LocalPath.Substring(1))); } - else if (uriSource.Uri.Scheme == "ms-appdata") + else if (uriSource.Uri?.Scheme == "ms-appdata") { var filePath = ResolveMsAppDataUri(uriSource.Uri); @@ -72,8 +72,14 @@ protected virtual void UpdateSource() asset = AVAsset.FromUrl(NSUrl.FromFilename(filePath)); } - else + else if (uriSource.Uri != null) + { asset = AVUrlAsset.Create(NSUrl.FromString(uriSource.Uri.AbsoluteUri)); + } + else + { + throw new InvalidOperationException($"{nameof(uriSource.Uri)} is not initialized"); + } } else { @@ -81,6 +87,8 @@ protected virtual void UpdateSource() asset = AVAsset.FromUrl(NSUrl.FromFilename(fileSource.File)); } + _ = asset ?? throw new NullReferenceException(); + playerItem = new AVPlayerItem(asset); AddStatusObserver(); @@ -109,7 +117,7 @@ protected virtual void UpdateSource() protected string ResolveMsAppDataUri(Uri uri) { - if (uri.Scheme == "ms-appdata") + if (uri.Scheme is "ms-appdata") { string filePath; @@ -133,7 +141,7 @@ protected virtual void ObserveRate(NSObservedChange e) { if (Controller is object) { - switch (avPlayerViewController.Player.Rate) + switch (avPlayerViewController.Player?.Rate) { case 0.0f: Controller.CurrentState = MediaElementState.Paused; @@ -150,7 +158,7 @@ protected virtual void ObserveRate(NSObservedChange e) void ObserveVolume(NSObservedChange e) { - if (Controller == null) + if (Controller == null || avPlayerViewController.Player == null) return; Controller.Volume = avPlayerViewController.Player.Volume; @@ -158,6 +166,7 @@ void ObserveVolume(NSObservedChange e) protected void ObserveStatus(NSObservedChange e) { + _ = avPlayerViewController.Player?.CurrentItem ?? throw new NullReferenceException(); Controller.Volume = avPlayerViewController.Player.Volume; switch (avPlayerViewController.Player.Status) @@ -220,7 +229,7 @@ void PlayedToEnd(NSNotification notification) } } - protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) + protected override void OnElementPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { switch (e.PropertyName) { @@ -252,7 +261,7 @@ protected override void OnElementPropertyChanged(object sender, System.Component } } - void MediaElementSeekRequested(object sender, SeekRequested e) + void MediaElementSeekRequested(object? sender, SeekRequested e) { if (avPlayerViewController.Player?.CurrentItem == null || avPlayerViewController.Player.Status != AVPlayerStatus.ReadyToPlay) return; @@ -301,7 +310,7 @@ void UpdateVolume() avPlayerViewController.Player.Volume = (float)Element.Volume; } - void MediaElementStateRequested(object sender, StateRequested e) + void MediaElementStateRequested(object? sender, StateRequested e) { switch (e.State) { @@ -325,8 +334,8 @@ void MediaElementStateRequested(object sender, StateRequested e) SetKeepScreenOn(false); // iOS has no stop... - avPlayerViewController?.Player.Pause(); - avPlayerViewController?.Player.Seek(CMTime.Zero); + avPlayerViewController.Player?.Pause(); + avPlayerViewController.Player?.Seek(CMTime.Zero); Controller.CurrentState = MediaElementState.Stopped; var err = AVAudioSession.SharedInstance().SetActive(false); @@ -354,7 +363,7 @@ void SeekComplete(bool finished) Controller?.OnSeekCompleted(); } - void MediaElementPositionRequested(object sender, EventArgs e) => Controller.Position = Position; + void MediaElementPositionRequested(object? sender, EventArgs e) => Controller.Position = Position; protected override void OnElementChanged(ElementChangedEventArgs e) { @@ -386,7 +395,7 @@ protected override void OnElementChanged(ElementChangedEventArgs e if (e.NewElement != null) { - SetNativeControl(avPlayerViewController.View); + SetNativeControl(avPlayerViewController?.View ?? throw new NullReferenceException()); Element.PropertyChanged += OnElementPropertyChanged; Element.SeekRequested += MediaElementSeekRequested; @@ -408,13 +417,13 @@ protected override void OnElementChanged(ElementChangedEventArgs e protected virtual void UpdateBackgroundColor() => BackgroundColor = Element.BackgroundColor.ToUIColor(); - protected void DisposeObservers(ref IDisposable disposable) + protected void DisposeObservers(ref IDisposable? disposable) { disposable?.Dispose(); disposable = null; } - protected void DisposeObservers(ref NSObject disposable) + protected void DisposeObservers(ref NSObject? disposable) { disposable?.Dispose(); disposable = null; @@ -437,7 +446,7 @@ void AddRateObserver() void AddStatusObserver() { DestroyStatusObserver(); - statusObserver = playerItem.AddObserver("status", NSKeyValueObservingOptions.New, ObserveStatus); + statusObserver = playerItem?.AddObserver("status", NSKeyValueObservingOptions.New, ObserveStatus); } void AddPlayedToEndObserver() diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/Android/PopupRenderer.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/Android/PopupRenderer.android.cs new file mode 100644 index 000000000..d69e2477c --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/Android/PopupRenderer.android.cs @@ -0,0 +1,344 @@ +using System; +using System.ComponentModel; +using Android.App; +using Android.Content; +using Android.Graphics.Drawables; +using Android.Views; +using Android.Widget; +using Xamarin.CommunityToolkit.UI.Views; +using Xamarin.Forms; +using Xamarin.Forms.Platform.Android; +using static Android.App.ActionBar; +using AColorRes = Android.Resource.Color; +using AView = Android.Views.View; +using FormsPlatform = Xamarin.Forms.Platform.Android.Platform; +using GravityFlags = Android.Views.GravityFlags; + +[assembly: ExportRenderer(typeof(BasePopup), typeof(PopupRenderer))] + +namespace Xamarin.CommunityToolkit.UI.Views +{ + public class PopupRenderer : Dialog, IVisualElementRenderer, IDialogInterfaceOnCancelListener + { + int? defaultLabelFor; + VisualElementTracker? tracker; + ContainerView? container; + bool isDisposed; + + public BasePopup? Element { get; private set; } + + void IVisualElementRenderer.UpdateLayout() => tracker?.UpdateLayout(); + + VisualElement? IVisualElementRenderer.Element => Element; + + AView? IVisualElementRenderer.View => container; + + ViewGroup? IVisualElementRenderer.ViewGroup => null; + + VisualElementTracker? IVisualElementRenderer.Tracker => tracker; + + public event EventHandler? ElementChanged; + + public event EventHandler? ElementPropertyChanged; + + public PopupRenderer(Context context) + : base(context) + { + } + + void IVisualElementRenderer.SetElement(VisualElement element) + { + if (element == null) + throw new ArgumentNullException(nameof(element)); + + if (element is not BasePopup popup) + throw new ArgumentNullException("Element is not of type " + typeof(BasePopup), nameof(element)); + + var oldElement = Element; + Element = popup; + CreateControl(Element); + + if (oldElement != null) + oldElement.PropertyChanged -= OnElementPropertyChanged; + + element.PropertyChanged += OnElementPropertyChanged; + + if (tracker == null) + tracker = new VisualElementTracker(this); + + OnElementChanged(new ElementChangedEventArgs(oldElement, Element)); + } + + protected virtual void OnElementChanged(ElementChangedEventArgs e) + { + if (e.NewElement != null && !isDisposed && Element is BasePopup basePopup) + { + SetEvents(basePopup); + SetColor(basePopup); + SetSize(basePopup); + SetAnchor(basePopup); + SetLightDismiss(basePopup); + + Show(); + } + + ElementChanged?.Invoke(this, new VisualElementChangedEventArgs(e.OldElement, e.NewElement)); + } + + public override void Show() + { + base.Show(); + Element?.OnOpened(); + } + + protected virtual void OnElementPropertyChanged(object? sender, PropertyChangedEventArgs args) + { + if (Element is BasePopup basePopup) + { + if (args.PropertyName == BasePopup.VerticalOptionsProperty.PropertyName + || args.PropertyName == BasePopup.HorizontalOptionsProperty.PropertyName + || args.PropertyName == BasePopup.SizeProperty.PropertyName) + { + SetSize(basePopup); + SetAnchor(basePopup); + } + else if (args.PropertyName == BasePopup.ColorProperty.PropertyName) + { + SetColor(basePopup); + } + } + + ElementPropertyChanged?.Invoke(this, args); + } + + void CreateControl(in BasePopup basePopup) + { + if (container == null) + { + container = new ContainerView(Context, basePopup.Content); + SetContentView(container); + } + } + + void SetEvents(in BasePopup basePopup) + { + SetOnCancelListener(this); + basePopup.Dismissed += OnDismissed; + } + + void SetColor(in BasePopup basePopup) => Window?.SetBackgroundDrawable(new ColorDrawable(basePopup.Color.ToAndroid(AColorRes.BackgroundLight, Context))); + + void SetSize(in BasePopup basePopup) + { + if (basePopup.Content != null && basePopup.Size != default) + { + var decorView = (ViewGroup)(Window?.DecorView ?? throw new NullReferenceException()); + var child = (FrameLayout)(decorView?.GetChildAt(0) ?? throw new NullReferenceException()); + + var realWidth = (int)Context.ToPixels(basePopup.Size.Width); + var realHeight = (int)Context.ToPixels(basePopup.Size.Height); + + var realContentWidth = (int)Context.ToPixels(basePopup.Content.WidthRequest); + var realContentHeight = (int)Context.ToPixels(basePopup.Content.HeightRequest); + + var childLayoutParams = (FrameLayout.LayoutParams)(child?.LayoutParameters ?? throw new NullReferenceException()); + childLayoutParams.Width = realWidth; + childLayoutParams.Height = realHeight; + child.LayoutParameters = childLayoutParams; + + var horizontalParams = -1; + switch (basePopup.Content.HorizontalOptions.Alignment) + { + case LayoutAlignment.Center: + case LayoutAlignment.End: + case LayoutAlignment.Start: + horizontalParams = LayoutParams.WrapContent; + break; + case LayoutAlignment.Fill: + horizontalParams = LayoutParams.MatchParent; + break; + } + + var verticalParams = -1; + switch (basePopup.Content.VerticalOptions.Alignment) + { + case LayoutAlignment.Center: + case LayoutAlignment.End: + case LayoutAlignment.Start: + verticalParams = LayoutParams.WrapContent; + break; + case LayoutAlignment.Fill: + verticalParams = LayoutParams.MatchParent; + break; + } + + _ = container ?? throw new NullReferenceException(); + if (realContentWidth > -1) + { + var inputMeasuredWidth = realContentWidth > realWidth ? + realWidth : realContentWidth; + container.Measure(inputMeasuredWidth, (int)MeasureSpecMode.Unspecified); + horizontalParams = container.MeasuredWidth; + } + else + { + container.Measure(realWidth, (int)MeasureSpecMode.Unspecified); + horizontalParams = container.MeasuredWidth > realWidth ? + realWidth : container.MeasuredWidth; + } + + if (realContentHeight > -1) + { + verticalParams = realContentHeight; + } + else + { + var inputMeasuredWidth = realContentWidth > -1 ? horizontalParams : realWidth; + container.Measure(inputMeasuredWidth, (int)MeasureSpecMode.Unspecified); + verticalParams = container.MeasuredHeight > realHeight ? + realHeight : container.MeasuredHeight; + } + + var containerLayoutParams = new FrameLayout.LayoutParams(horizontalParams, verticalParams); + + switch (basePopup.Content.VerticalOptions.Alignment) + { + case LayoutAlignment.Start: + containerLayoutParams.Gravity = GravityFlags.Top; + break; + case LayoutAlignment.Center: + case LayoutAlignment.Fill: + containerLayoutParams.Gravity = GravityFlags.FillVertical; + containerLayoutParams.Height = realHeight; + container.MatchHeight = true; + break; + case LayoutAlignment.End: + containerLayoutParams.Gravity = GravityFlags.Bottom; + break; + } + + switch (basePopup.Content.HorizontalOptions.Alignment) + { + case LayoutAlignment.Start: + containerLayoutParams.Gravity |= GravityFlags.Left; + break; + case LayoutAlignment.Center: + case LayoutAlignment.Fill: + containerLayoutParams.Gravity |= GravityFlags.FillHorizontal; + containerLayoutParams.Width = realWidth; + container.MatchWidth = true; + break; + case LayoutAlignment.End: + containerLayoutParams.Gravity |= GravityFlags.Right; + break; + } + + container.LayoutParameters = containerLayoutParams; + } + } + + void SetAnchor(in BasePopup basePopup) + { + if (basePopup.Anchor != null) + { + var anchorView = FormsPlatform.GetRenderer(Element?.Anchor).View; + var locationOnScreen = new int[2]; + anchorView.GetLocationOnScreen(locationOnScreen); + + Window?.SetGravity(GravityFlags.Top | GravityFlags.Left); + Window?.DecorView.Measure((int)MeasureSpecMode.Unspecified, (int)MeasureSpecMode.Unspecified); + + // This logic is tricky, please read these notes if you need to modify + // Android window coordinate starts (0,0) at the top left and (max,max) at the bottom right. All of the positions + // that are being handled in this operation assume the point is at the top left of the rectangle. This means the + // calculation operates in this order: + // 1. Calculate top-left position of Anchor + // 2. Calculate the Actual Center of the Anchor by adding the width /2 and height / 2 + // 3. Calculate the top-left point of where the dialog should be positioned by subtracting the Width / 2 and height / 2 + // of the dialog that is about to be drawn. + _ = Window?.Attributes ?? throw new NullReferenceException(); + + Window.Attributes.X = locationOnScreen[0] + (anchorView.Width / 2) - (Window.DecorView.MeasuredWidth / 2); + Window.Attributes.Y = locationOnScreen[1] + (anchorView.Height / 2) - (Window.DecorView.MeasuredHeight / 2); + } + else + { + SetDialogPosition(basePopup); + } + } + + void SetLightDismiss(in BasePopup basePopup) + { + if (basePopup.IsLightDismissEnabled) + return; + + SetCancelable(false); + SetCanceledOnTouchOutside(false); + } + + void SetDialogPosition(in BasePopup basePopup) + { + var gravityFlags = basePopup.VerticalOptions.Alignment switch + { + LayoutAlignment.Start => GravityFlags.Top, + LayoutAlignment.End => GravityFlags.Bottom, + _ => GravityFlags.CenterVertical, + }; + gravityFlags |= basePopup.HorizontalOptions.Alignment switch + { + LayoutAlignment.Start => GravityFlags.Left, + LayoutAlignment.End => GravityFlags.Right, + _ => GravityFlags.CenterHorizontal, + }; + Window?.SetGravity(gravityFlags); + } + + void OnDismissed(object? sender, PopupDismissedEventArgs e) => Dismiss(); + + public void OnCancel(IDialogInterface? dialog) + { + if (Element?.IsLightDismissEnabled is true) + Element.LightDismiss(); + } + + protected override void Dispose(bool disposing) + { + if (isDisposed) + return; + + isDisposed = true; + if (disposing) + { + tracker?.Dispose(); + tracker = null; + + if (Element != null) + { + Element.PropertyChanged -= OnElementPropertyChanged; + Element = null; + } + } + + base.Dispose(disposing); + } + + SizeRequest IVisualElementRenderer.GetDesiredSize(int widthConstraint, int heightConstraint) + { + if (isDisposed || container == null) + return default(SizeRequest); + + container.Measure(widthConstraint, heightConstraint); + return new SizeRequest(new Size(container.MeasuredWidth, container.MeasuredHeight), default(Size)); + } + + void IVisualElementRenderer.SetLabelFor(int? id) + { + _ = container ?? throw new NullReferenceException(); + + if (defaultLabelFor == null) + defaultLabelFor = container.LabelFor; + + container.LabelFor = (int)(id ?? defaultLabelFor); + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/BasePopup.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/BasePopup.shared.cs new file mode 100644 index 000000000..f56bfe2db --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/BasePopup.shared.cs @@ -0,0 +1,180 @@ +using System; +using Xamarin.CommunityToolkit.Helpers; +using Xamarin.Forms; + +namespace Xamarin.CommunityToolkit.UI.Views +{ + /// + /// The popup control's base implementation. + /// + [ContentProperty(nameof(Content))] + public abstract class BasePopup : VisualElement, IElementConfiguration + { + readonly WeakEventManager dismissWeakEventManager = new WeakEventManager(); + readonly WeakEventManager openedWeakEventManager = new WeakEventManager(); + + /// + /// Instantiates a new instance of . + /// + protected BasePopup() + { + VerticalOptions = LayoutOptions.CenterAndExpand; + HorizontalOptions = LayoutOptions.CenterAndExpand; + IsLightDismissEnabled = true; + platformConfigurationRegistry = new Lazy>(() => new PlatformConfigurationRegistry(this)); + } + + readonly Lazy> platformConfigurationRegistry; + + public IPlatformElementConfiguration On() where T : IConfigPlatform => + platformConfigurationRegistry.Value.On(); + + public static readonly BindableProperty ContentProperty = BindableProperty.Create(nameof(Content), typeof(View), typeof(BasePopup), propertyChanged: OnContentChanged); + + public static readonly BindableProperty ColorProperty = BindableProperty.Create(nameof(Color), typeof(Color), typeof(BasePopup), Color.Default); + public static readonly BindableProperty SizeProperty = BindableProperty.Create(nameof(Size), typeof(Size), typeof(BasePopup), default(Size)); + + public static readonly BindableProperty VerticalOptionsProperty = BindableProperty.Create(nameof(VerticalOptions), typeof(LayoutOptions), typeof(BasePopup), LayoutOptions.CenterAndExpand); + public static readonly BindableProperty HorizontalOptionsProperty = BindableProperty.Create(nameof(HorizontalOptions), typeof(LayoutOptions), typeof(BasePopup), LayoutOptions.CenterAndExpand); + + /// + /// Gets or sets the content to render in the Popup. + /// + /// + /// The View can be or type: , or + /// + public virtual View? Content + { + get => (View?)GetValue(ContentProperty); + set => SetValue(ContentProperty, value); + } + + /// + /// Gets or sets the of the Popup. + /// + /// + /// This color sets the native background color of the , which is + /// independent of any background color configured in the actual View. + /// + public Color Color + { + get => (Color)GetValue(ColorProperty); + set => SetValue(ColorProperty, value); + } + + /// + /// Gets or sets the for positioning the vertically on the screen. + /// + public LayoutOptions VerticalOptions + { + get => (LayoutOptions)GetValue(VerticalOptionsProperty); + set => SetValue(VerticalOptionsProperty, value); + } + + /// + /// Gets or sets the for positioning the horizontally on the screen. + /// + public LayoutOptions HorizontalOptions + { + get => (LayoutOptions)GetValue(HorizontalOptionsProperty); + set => SetValue(HorizontalOptionsProperty, value); + } + + /// + /// Gets or sets the anchor. + /// + /// + /// The Anchor is where the Popup will render closest to. When an Anchor is configured + /// the popup will appear centered over that control or as close as possible. + /// + public View? Anchor { get; set; } + + /// + /// Gets or sets the of the Popup Display. + /// + /// + /// The Popup will always try to constrain the actual size of the + /// to the of the View unless a is specified. + /// If the contiains a + /// will be required. This will allow the View to have a concept of + /// that varies from the actual of the + /// + public Size Size + { + get => (Size)GetValue(SizeProperty); + set => SetValue(SizeProperty, value); + } + + /// + /// Gets or sets a value indicating whether the popup can be light dismissed. + /// + /// + /// When true and the user taps outside of the popup it will dismiss. + /// + public bool IsLightDismissEnabled { get; set; } + + /// + /// Dismissed event is invoked when the popup is closed. + /// + public event EventHandler Dismissed + { + add => dismissWeakEventManager.AddEventHandler(value); + remove => dismissWeakEventManager.RemoveEventHandler(value); + } + + /// + /// Opened event is invoked when the popup is opened. + /// + public event EventHandler Opened + { + add => openedWeakEventManager.AddEventHandler(value); + remove => openedWeakEventManager.RemoveEventHandler(value); + } + + /// + /// Invokes the event. + /// + /// + /// The results to add to the . + /// + protected void OnDismissed(object? result) => + dismissWeakEventManager.RaiseEvent(this, new PopupDismissedEventArgs(result), nameof(Dismissed)); + + /// + /// Invokes the event. + /// + internal virtual void OnOpened() => + openedWeakEventManager.RaiseEvent(this, new PopupOpenedEventArgs(), nameof(Opened)); + + /// + /// Invoked when the popup is light dismissed. In other words when the + /// user taps outside of the popup and it closes. + /// + protected internal virtual void LightDismiss() + { + // Note 1/9/2021 + // Left empty by design + // + // This method needs to be left empty as it is not + // required for a child class to have an implementation + // of LightDismiss. This means if a renderer invokes + // this method nothing will happen unless the child + // class has an implementation. + } + + /// + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + + if (Content != null) + SetInheritedBindingContext(Content, BindingContext); + } + + static void OnContentChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is BasePopup popup) + popup.OnBindingContextChanged(); + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/Popup.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/Popup.shared.cs new file mode 100644 index 000000000..a92c4955b --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/Popup.shared.cs @@ -0,0 +1,10 @@ +namespace Xamarin.CommunityToolkit.UI.Views +{ + /// + /// Default popup implementation that uses a + /// generic result. + /// + public class Popup : Popup + { + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/PopupDismissedEventArgs.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/PopupDismissedEventArgs.shared.cs new file mode 100644 index 000000000..a6ea5783e --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/PopupDismissedEventArgs.shared.cs @@ -0,0 +1,35 @@ +using System; + +namespace Xamarin.CommunityToolkit.UI.Views +{ + /// + /// Popup dismissed event arguments used when a popup is dismissed. + /// + public class PopupDismissedEventArgs : PopupDismissedEventArgs + { + public PopupDismissedEventArgs(object? result) + : base(result) + { + } + } + + /// + /// Popup dismissed event arguments used when a popup is dismissed. + /// + public class PopupDismissedEventArgs : EventArgs + { + /// + /// Initialization an instance of . + /// + /// + /// The result of the popup. + /// + public PopupDismissedEventArgs(T result) => + Result = result; + + /// + /// The resulting object to return. + /// + public T Result { get; } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/PopupOfT.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/PopupOfT.shared.cs new file mode 100644 index 000000000..d2c62dd92 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/PopupOfT.shared.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; + +namespace Xamarin.CommunityToolkit.UI.Views +{ + /// + public abstract class Popup : BasePopup + { + TaskCompletionSource taskCompletionSource; + + /// + /// Initalizes a default implementation of . + /// + protected Popup() => + taskCompletionSource = new TaskCompletionSource(); + + /// + /// Resets the Popup. + /// + public void Reset() => + taskCompletionSource = new TaskCompletionSource(); + + /// + /// Dismiss the current popup. + /// + /// + /// The result to return. + /// + public void Dismiss(T? result) + { + taskCompletionSource.TrySetResult(result); + OnDismissed(result); + } + + /// + /// Gets the final result of the dismissed popup. + /// + public Task Result => taskCompletionSource.Task; + + /// + protected internal override void LightDismiss() => + taskCompletionSource.TrySetResult(GetLightDismissResult()); + + /// + /// Gets the light dismiss default result. + /// + /// + /// The light dismiss value of . + /// + /// + /// When a user dismisses the Popup via the light dismiss, this + /// method will return a default value. + /// + protected virtual T? GetLightDismissResult() => default(T); + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/PopupOpenedEventArgs.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/PopupOpenedEventArgs.shared.cs new file mode 100644 index 000000000..ced827005 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/PopupOpenedEventArgs.shared.cs @@ -0,0 +1,11 @@ +using System; + +namespace Xamarin.CommunityToolkit.UI.Views +{ + /// + /// Popup opened event arguments used when a popup is opened. + /// + public class PopupOpenedEventArgs : EventArgs + { + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/UWP/PopupRenderer.uwp.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/UWP/PopupRenderer.uwp.cs new file mode 100644 index 000000000..243b666cc --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/UWP/PopupRenderer.uwp.cs @@ -0,0 +1,291 @@ +using System; +using System.ComponentModel; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Xamarin.CommunityToolkit.UI.Views; +using Xamarin.Forms; +using Xamarin.Forms.Internals; +using Xamarin.Forms.Platform.UWP; +using UWPThickness = Windows.UI.Xaml.Thickness; +using XamlStyle = Windows.UI.Xaml.Style; +using Specific = Xamarin.CommunityToolkit.PlatformConfiguration.WindowsSpecific.PopUp; + +[assembly: ExportRenderer(typeof(BasePopup), typeof(PopupRenderer))] + +namespace Xamarin.CommunityToolkit.UI.Views +{ + public class PopupRenderer : Flyout, IVisualElementRenderer + { + const double defaultBorderThickness = 2; + const double defaultSize = 600; + bool isDisposed = false; + XamlStyle? flyoutStyle; + XamlStyle? panelStyle; + + public BasePopup? Element { get; private set; } + + internal ViewToRendererConverter.WrapperControl? Control { get; private set; } + + FrameworkElement? IVisualElementRenderer.ContainerElement => null; + + VisualElement? IVisualElementRenderer.Element => Element; + + public event EventHandler? ElementChanged; + + public event EventHandler? ElementPropertyChanged; + + public PopupRenderer() + { + } + + void IVisualElementRenderer.SetElement(VisualElement element) + { + if (element == null) + throw new ArgumentNullException(nameof(element)); + + if (element is not BasePopup popup) + throw new ArgumentNullException("Element is not of type " + typeof(BasePopup), nameof(element)); + + var oldElement = Element; + Element = popup; + CreateControl(); + + if (oldElement != null) + oldElement.PropertyChanged -= OnElementPropertyChanged; + + element.PropertyChanged += OnElementPropertyChanged; + + OnElementChanged(new ElementChangedEventArgs(oldElement, Element)); + } + + void CreateControl() + { + if (Control == null && Element?.Content != null) + { + Control = new ViewToRendererConverter.WrapperControl(Element.Content); + Content = Control; + } + } + + void InitializeStyles() + { + flyoutStyle = new XamlStyle { TargetType = typeof(FlyoutPresenter) }; + panelStyle = new XamlStyle { TargetType = typeof(Panel) }; + } + + protected virtual void OnElementChanged(ElementChangedEventArgs e) + { + if (e.NewElement != null && !isDisposed) + { + ConfigureControl(); + Show(); + } + + ElementChanged?.Invoke(this, new VisualElementChangedEventArgs(e.OldElement, e.NewElement)); + } + + protected virtual void OnElementPropertyChanged(object? sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == BasePopup.VerticalOptionsProperty.PropertyName || + args.PropertyName == BasePopup.HorizontalOptionsProperty.PropertyName || + args.PropertyName == BasePopup.SizeProperty.PropertyName || + args.PropertyName == BasePopup.ColorProperty.PropertyName) + { + ConfigureControl(); + } + + ElementPropertyChanged?.Invoke(this, args); + } + + void ConfigureControl() + { + InitializeStyles(); + SetEvents(); + SetColor(); + SetBorderColor(); + SetSize(); + SetLayout(); + ApplyStyles(); + } + + void SetEvents() + { + if (Element?.IsLightDismissEnabled is true) + Closing += OnClosing; + + if (Element != null) + Element.Dismissed += OnDismissed; + } + + void SetSize() + { + _ = Element ?? throw new InvalidOperationException($"{nameof(Element)} cannot be null"); + _ = Control ?? throw new InvalidOperationException($"{nameof(Control)} cannot be null"); + _ = flyoutStyle ?? throw new InvalidOperationException($"{nameof(flyoutStyle)} cannot be null"); + var standardSize = new Size { Width = defaultSize, Height = defaultSize / 2 }; + + var currentSize = Element.Size != default(Size) ? Element.Size : standardSize; + Control.Width = currentSize.Width; + Control.Height = currentSize.Height; + + flyoutStyle.Setters.Add(new Windows.UI.Xaml.Setter(FlyoutPresenter.MinHeightProperty, currentSize.Height + (defaultBorderThickness * 2))); + flyoutStyle.Setters.Add(new Windows.UI.Xaml.Setter(FlyoutPresenter.MinWidthProperty, currentSize.Width + (defaultBorderThickness * 2))); + flyoutStyle.Setters.Add(new Windows.UI.Xaml.Setter(FlyoutPresenter.MaxHeightProperty, currentSize.Height + (defaultBorderThickness * 2))); + flyoutStyle.Setters.Add(new Windows.UI.Xaml.Setter(FlyoutPresenter.MaxWidthProperty, currentSize.Width + (defaultBorderThickness * 2))); + } + + void SetLayout() + { + LightDismissOverlayMode = LightDismissOverlayMode.On; + + if (Element != null) + SetDialogPosition(Element.VerticalOptions, Element.HorizontalOptions); + } + + void SetBorderColor() + { + _ = flyoutStyle ?? throw new NullReferenceException(); + + flyoutStyle.Setters.Add(new Windows.UI.Xaml.Setter(FlyoutPresenter.PaddingProperty, 0)); + flyoutStyle.Setters.Add(new Windows.UI.Xaml.Setter(FlyoutPresenter.BorderThicknessProperty, new UWPThickness(defaultBorderThickness))); + + if (Element == null) + { + Log.Warning("warning", "The PopUpView is null."); + return; + } + + var borderColor = Specific.GetBorderColor(Element); + if (borderColor == default(Color)) + flyoutStyle.Setters.Add(new Windows.UI.Xaml.Setter(FlyoutPresenter.BorderBrushProperty, Color.FromHex("#2e6da0").ToWindowsColor())); + else + flyoutStyle.Setters.Add(new Windows.UI.Xaml.Setter(FlyoutPresenter.BorderBrushProperty, borderColor.ToWindowsColor())); + } + + void SetColor() + { + _ = Element?.Content ?? throw new NullReferenceException(); + _ = panelStyle ?? throw new NullReferenceException(); + _ = flyoutStyle ?? throw new NullReferenceException(); + + if (Element.Content.BackgroundColor == default(Color)) + panelStyle.Setters.Add(new Windows.UI.Xaml.Setter(Panel.BackgroundProperty, Element.Color.ToWindowsColor())); + + flyoutStyle.Setters.Add(new Windows.UI.Xaml.Setter(FlyoutPresenter.BackgroundProperty, Element.Color.ToWindowsColor())); + +#if UWP_18362 + if (Element.Color == Color.Transparent) + flyoutStyle.Setters.Add(new Windows.UI.Xaml.Setter(FlyoutPresenter.IsDefaultShadowEnabledProperty, false)); +#endif + } + + void ApplyStyles() + { + _ = Control ?? throw new NullReferenceException(); + Control.Style = panelStyle; + FlyoutPresenterStyle = flyoutStyle; + } + + void Show() + { + if (Element?.Anchor != null) + { + var anchor = Platform.GetRenderer(Element.Anchor).ContainerElement; + FlyoutBase.SetAttachedFlyout(anchor, this); + FlyoutBase.ShowAttachedFlyout(anchor); + } + else + { + var frameworkElement = Platform.GetRenderer(Element?.Parent as VisualElement)?.ContainerElement; + FlyoutBase.SetAttachedFlyout(frameworkElement, this); + FlyoutBase.ShowAttachedFlyout(frameworkElement); + } + + Element?.OnOpened(); + } + + void SetDialogPosition(LayoutOptions verticalOptions, LayoutOptions horizontalOptions) + { + if (IsTopLeft()) + Placement = FlyoutPlacementMode.TopEdgeAlignedLeft; + else if (IsTop()) + Placement = FlyoutPlacementMode.Top; + else if (IsTopRight()) + Placement = FlyoutPlacementMode.TopEdgeAlignedRight; + else if (IsRight()) + Placement = FlyoutPlacementMode.Right; + else if (IsBottomRight()) + Placement = FlyoutPlacementMode.BottomEdgeAlignedRight; + else if (IsBottom()) + Placement = FlyoutPlacementMode.Bottom; + else if (IsBottomLeft()) + Placement = FlyoutPlacementMode.BottomEdgeAlignedLeft; + else if (IsLeft()) + Placement = FlyoutPlacementMode.Left; + else if (Element != null && Element.Anchor == null) + Placement = FlyoutPlacementMode.Full; + else + Placement = FlyoutPlacementMode.Top; + + bool IsTopLeft() => verticalOptions.Alignment == LayoutAlignment.Start && horizontalOptions.Alignment == LayoutAlignment.Start; + bool IsTop() => verticalOptions.Alignment == LayoutAlignment.Start && horizontalOptions.Alignment == LayoutAlignment.Center; + bool IsTopRight() => verticalOptions.Alignment == LayoutAlignment.Start && horizontalOptions.Alignment == LayoutAlignment.End; + bool IsRight() => verticalOptions.Alignment == LayoutAlignment.Center && horizontalOptions.Alignment == LayoutAlignment.End; + bool IsBottomRight() => verticalOptions.Alignment == LayoutAlignment.End && horizontalOptions.Alignment == LayoutAlignment.End; + bool IsBottom() => verticalOptions.Alignment == LayoutAlignment.End && horizontalOptions.Alignment == LayoutAlignment.Center; + bool IsBottomLeft() => verticalOptions.Alignment == LayoutAlignment.End && horizontalOptions.Alignment == LayoutAlignment.Start; + bool IsLeft() => verticalOptions.Alignment == LayoutAlignment.Center && horizontalOptions.Alignment == LayoutAlignment.Start; + } + + SizeRequest IVisualElementRenderer.GetDesiredSize(double widthConstraint, double heightConstraint) + { + if (isDisposed || Control == null) + return default(SizeRequest); + + var constraint = new Windows.Foundation.Size(widthConstraint, heightConstraint); + Control.Measure(constraint); + + var size = new Size(Math.Ceiling(Control.DesiredSize.Width), Math.Ceiling(Control.DesiredSize.Height)); + return new SizeRequest(size); + } + + UIElement? IVisualElementRenderer.GetNativeElement() => Control; + + void OnDismissed(object? sender, PopupDismissedEventArgs e) + { + Hide(); + } + + void OnClosing(object? sender, object e) + { + if (IsOpen && Element?.IsLightDismissEnabled is true) + Element.LightDismiss(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!isDisposed && disposing) + { + if (Element != null) + Element.Dismissed -= OnDismissed; + + if (Control != null) + Control.CleanUp(); + + Element = null; + Control = null; + + Closed -= OnClosing; + } + + isDisposed = true; + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/iOS/PopupRenderer.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/iOS/PopupRenderer.ios.cs new file mode 100644 index 000000000..e59135aa9 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Popup/iOS/PopupRenderer.ios.cs @@ -0,0 +1,287 @@ +using System; +using System.ComponentModel; +using CoreGraphics; +using UIKit; +using Xamarin.CommunityToolkit.Helpers; +using Xamarin.CommunityToolkit.UI.Views; +using Xamarin.Forms; +using Xamarin.Forms.Internals; +using Xamarin.Forms.Platform.iOS; +using Xamarin.CommunityToolkit.PlatformConfiguration.iOSSpecific; +using Specifics = Xamarin.CommunityToolkit.PlatformConfiguration.iOSSpecific.PopUp; + +[assembly: ExportRenderer(typeof(BasePopup), typeof(PopupRenderer))] + +namespace Xamarin.CommunityToolkit.UI.Views +{ + public class PopupRenderer : UIViewController, IVisualElementRenderer + { + bool isDisposed; + + public IVisualElementRenderer? Control { get; private set; } + + public BasePopup? Element { get; private set; } + + VisualElement? IVisualElementRenderer.Element => Element; + + public UIView? NativeView => View; + + public UIViewController? ViewController { get; private set; } + + public event EventHandler? ElementChanged; + + public event EventHandler? ElementPropertyChanged; + + [Preserve(Conditional = true)] + public PopupRenderer() + { + } + + public void SetElementSize(Size size) => + Control?.SetElementSize(size); + + public override void ViewDidLayoutSubviews() + { + base.ViewDidLayoutSubviews(); + + _ = View ?? throw new InvalidOperationException($"{nameof(View)} cannot be null"); + SetElementSize(new Size(View.Bounds.Width, View.Bounds.Height)); + } + + public override void ViewDidAppear(bool animated) + { + base.ViewDidAppear(animated); + + _ = Element ?? throw new InvalidOperationException($"{nameof(Element)} cannot be null"); + ModalInPopover = !Element.IsLightDismissEnabled; + } + + public SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) => + NativeView.GetSizeRequest(widthConstraint, heightConstraint); + + void IVisualElementRenderer.SetElement(VisualElement element) + { + if (element == null) + throw new ArgumentNullException(nameof(element)); + + if (element is not BasePopup popup) + throw new ArgumentNullException("Element is not of type " + typeof(BasePopup), nameof(element)); + + var oldElement = Element; + Element = popup; + CreateControl(); + + if (oldElement != null) + oldElement.PropertyChanged -= OnElementPropertyChanged; + + element.PropertyChanged += OnElementPropertyChanged; + + OnElementChanged(new ElementChangedEventArgs(oldElement, Element)); + } + + protected virtual void OnElementChanged(ElementChangedEventArgs e) + { + if (e.NewElement != null && !isDisposed) + { + ModalInPopover = true; + ModalPresentationStyle = UIModalPresentationStyle.Popover; + + SetViewController(); + SetPresentationController(); + SetEvents(); + SetSize(); + SetLayout(); + SetBackgroundColor(); + SetView(); + AddToCurrentPageViewController(); + } + + ElementChanged?.Invoke(this, new VisualElementChangedEventArgs(e.OldElement, e.NewElement)); + } + + protected virtual void OnElementPropertyChanged(object? sender, PropertyChangedEventArgs args) + { + if (args.PropertyName == BasePopup.VerticalOptionsProperty.PropertyName || + args.PropertyName == BasePopup.HorizontalOptionsProperty.PropertyName) + { + SetLayout(); + } + else if (args.PropertyName == BasePopup.SizeProperty.PropertyName) + { + SetSize(); + } + else if (args.PropertyName == BasePopup.ColorProperty.PropertyName) + { + SetBackgroundColor(); + } + + ElementPropertyChanged?.Invoke(this, args); + } + + void CreateControl() + { + _ = Element ?? throw new InvalidOperationException($"{nameof(Element)} cannot be null"); + + var view = Element.Content; + var contentPage = new ContentPage { Content = view, Padding = new Thickness(25) }; + + Control = Platform.CreateRenderer(contentPage); + Platform.SetRenderer(contentPage, Control); + contentPage.Parent = Application.Current.MainPage; + contentPage.SetBinding(VisualElement.BindingContextProperty, new Binding { Source = Element, Path = VisualElement.BindingContextProperty.PropertyName }); + } + + void SetViewController() + { + var currentPageRenderer = Platform.GetRenderer(Application.Current.MainPage); + ViewController = currentPageRenderer.ViewController; + } + + void SetEvents() + { + _ = Element ?? throw new InvalidOperationException($"{nameof(Element)} cannot be null"); + Element.Dismissed += OnDismissed; + } + + void SetSize() + { + _ = Element ?? throw new InvalidOperationException($"{nameof(Element)} cannot be null"); + if (!Element.Size.IsZero) + { + PreferredContentSize = new CGSize(Element.Size.Width, Element.Size.Height); + } + } + + void SetLayout() + { + ((UIPopoverPresentationController)PresentationController).SourceRect = new CGRect(0, 0, PreferredContentSize.Width, PreferredContentSize.Height); + + _ = Element ?? throw new InvalidOperationException($"{nameof(Element)} cannot be null"); + if (Element.Anchor == null) + { + var originY = Element.VerticalOptions.Alignment switch + { + LayoutAlignment.End => UIScreen.MainScreen.Bounds.Height, + LayoutAlignment.Center => UIScreen.MainScreen.Bounds.Height / 2, + _ => 0f + }; + + var originX = Element.HorizontalOptions.Alignment switch + { + LayoutAlignment.End => UIScreen.MainScreen.Bounds.Width, + LayoutAlignment.Center => UIScreen.MainScreen.Bounds.Width / 2, + _ => 0f + }; + + PopoverPresentationController.SourceRect = new CGRect(originX, originY, 0, 0); + PopoverPresentationController.PermittedArrowDirections = 0; + } + else + { + var view = Platform.GetRenderer(Element.Anchor).NativeView; + PopoverPresentationController.SourceView = view; + PopoverPresentationController.SourceRect = view.Bounds; + var arrowDirection = Specifics.GetArrowDirection(Element); + PopoverPresentationController.PermittedArrowDirections = arrowDirection switch + { + PopoverArrowDirection.Up => UIPopoverArrowDirection.Up, + PopoverArrowDirection.Down => UIPopoverArrowDirection.Down, + PopoverArrowDirection.Left => UIPopoverArrowDirection.Left, + PopoverArrowDirection.Right => UIPopoverArrowDirection.Right, + PopoverArrowDirection.Any => UIPopoverArrowDirection.Any, + PopoverArrowDirection.Unknown => UIPopoverArrowDirection.Unknown, + _ => 0 + }; + } + } + + void SetBackgroundColor() + { + _ = Element ?? throw new InvalidOperationException($"{nameof(Element)} cannot be null"); + _ = Control ?? throw new InvalidOperationException($"{nameof(Control)} cannot be null"); + + Control.NativeView.BackgroundColor = Element.Color.ToUIColor(); + } + + void SetView() + { + _ = View ?? throw new InvalidOperationException($"{nameof(View)} cannot be null"); + _ = Control ?? throw new InvalidOperationException($"{nameof(Control)} cannot be null"); + + View.AddSubview(Control.ViewController.View ?? throw new NullReferenceException()); + View.Bounds = new CGRect(0, 0, PreferredContentSize.Width, PreferredContentSize.Height); + AddChildViewController(Control.ViewController); + } + + void SetPresentationController() + { + var popOverDelegate = new PopoverDelegate(); + popOverDelegate.PopoverDismissed += HandlePopoverDelegateDismissed; + + ((UIPopoverPresentationController)PresentationController).SourceView = ViewController?.View ?? throw new NullReferenceException(); + + ((UIPopoverPresentationController)PresentationController).Delegate = popOverDelegate; + } + + void HandlePopoverDelegateDismissed(object? sender, UIPresentationController e) + { + _ = Element ?? throw new NullReferenceException(); + + if (IsViewLoaded && Element.IsLightDismissEnabled) + Element.LightDismiss(); + } + + void AddToCurrentPageViewController() + { + _ = ViewController ?? throw new InvalidOperationException($"{nameof(ViewController)} cannot be null"); + _ = Element ?? throw new InvalidOperationException($"{nameof(Element)} cannot be null"); + + ViewController.PresentViewController(this, true, () => Element.OnOpened()); + } + + async void OnDismissed(object? sender, PopupDismissedEventArgs e) + { + if (ViewController != null) + await ViewController.DismissViewControllerAsync(true); + } + + protected override void Dispose(bool disposing) + { + if (isDisposed) + return; + + isDisposed = true; + if (disposing) + { + if (Element != null) + { + Element.PropertyChanged -= OnElementPropertyChanged; + Element = null; + + var presentationController = (UIPopoverPresentationController)PresentationController; + if (presentationController != null) + presentationController.Delegate = null; + } + } + + base.Dispose(disposing); + } + + class PopoverDelegate : UIPopoverPresentationControllerDelegate + { + readonly WeakEventManager popoverDismissedEventManager = new WeakEventManager(); + + public event EventHandler PopoverDismissed + { + add => popoverDismissedEventManager.AddEventHandler(value); + remove => popoverDismissedEventManager.RemoveEventHandler(value); + } + + public override UIModalPresentationStyle GetAdaptivePresentationStyle(UIPresentationController forPresentationController) => + UIModalPresentationStyle.None; + + public override void DidDismiss(UIPresentationController presentationController) => + popoverDismissedEventManager.RaiseEvent(this, presentationController, nameof(PopoverDismissed)); + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/RangeSlider/RangeSlider.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/RangeSlider/RangeSlider.shared.cs index ef142e338..802e5ba78 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/RangeSlider/RangeSlider.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/RangeSlider/RangeSlider.shared.cs @@ -16,23 +16,23 @@ public class RangeSlider : BaseTemplatedView const double disabledOpacity = .6; - public event EventHandler ValueChanged; + public event EventHandler? ValueChanged; - public event EventHandler LowerValueChanged; + public event EventHandler? LowerValueChanged; - public event EventHandler UpperValueChanged; + public event EventHandler? UpperValueChanged; - public event EventHandler DragStarted; + public event EventHandler? DragStarted; - public event EventHandler LowerDragStarted; + public event EventHandler? LowerDragStarted; - public event EventHandler UpperDragStarted; + public event EventHandler? UpperDragStarted; - public event EventHandler DragCompleted; + public event EventHandler? DragCompleted; - public event EventHandler LowerDragCompleted; + public event EventHandler? LowerDragCompleted; - public event EventHandler UpperDragCompleted; + public event EventHandler? UpperDragCompleted; public static BindableProperty MinimumValueProperty = BindableProperty.Create(nameof(MinimumValue), typeof(double), typeof(RangeSlider), .0, propertyChanged: OnMinimumMaximumValuePropertyChanged); @@ -278,15 +278,15 @@ public string ValueLabelStringFormat set => SetValue(ValueLabelStringFormatProperty, value); } - public View LowerThumbView + public View? LowerThumbView { - get => (View)GetValue(LowerThumbViewProperty); + get => (View?)GetValue(LowerThumbViewProperty); set => SetValue(LowerThumbViewProperty, value); } - public View UpperThumbView + public View? UpperThumbView { - get => (View)GetValue(UpperThumbViewProperty); + get => (View?)GetValue(UpperThumbViewProperty); set => SetValue(UpperThumbViewProperty, value); } @@ -334,7 +334,7 @@ public double TrackRadius double TrackWidth => Width - LowerThumb.Width - UpperThumb.Width; - protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) + protected override void OnPropertyChanged([CallerMemberName] string propertyName = "") { base.OnPropertyChanged(propertyName); switch (propertyName) @@ -537,7 +537,7 @@ void OnLayoutPropertyChanged() BatchCommit(); } - void OnViewSizeChanged(object sender, System.EventArgs e) + void OnViewSizeChanged(object? sender, System.EventArgs e) { var maxHeight = Max(LowerValueLabel.Height, UpperValueLabel.Height); if ((sender == LowerValueLabel || sender == UpperValueLabel) && labelMaxHeight == maxHeight) @@ -550,9 +550,10 @@ void OnViewSizeChanged(object sender, System.EventArgs e) OnLayoutPropertyChanged(); } - void OnPanUpdated(object sender, PanUpdatedEventArgs e) + void OnPanUpdated(object? sender, PanUpdatedEventArgs e) { - var view = (View)sender; + var view = (View)(sender ?? throw new NullReferenceException($"{nameof(sender)} cannot be null")); + switch (e.StatusType) { case GestureStatus.Started: @@ -633,7 +634,7 @@ Color GetColorOrDefault(Color color, Color defaultColor) ? defaultSize : value; - void RaiseEvent(EventHandler eventHandler) + void RaiseEvent(EventHandler? eventHandler) => eventHandler?.Invoke(this, EventArgs.Empty); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/RangeSlider/ThumbFrame.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/RangeSlider/ThumbFrame.shared.cs index 09b9dcd38..3b8ede1e3 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/RangeSlider/ThumbFrame.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/RangeSlider/ThumbFrame.shared.cs @@ -1,4 +1,5 @@ -using Xamarin.Forms; +using System; +using Xamarin.Forms; namespace Xamarin.CommunityToolkit.UI.Views { @@ -9,7 +10,7 @@ public ThumbFrame() #region Required work-around to prevent linker from removing the platform-specific implementation #if __ANDROID__ if (System.DateTime.Now.Ticks < 0) - _ = new ThumbFrameRenderer(null); + _ = new ThumbFrameRenderer(ToolkitPlatform.Context ?? throw new NullReferenceException()); #endif #endregion } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Shield.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Shield.shared.cs index 5653c8460..c9c9c3556 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Shield.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Shield.shared.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows.Input; using Xamarin.CommunityToolkit.UI.Views.Internals; @@ -23,12 +24,62 @@ public class Shield : BaseTemplatedView /// /// Text that is shown on the left side of the . This is a bindable property. /// - public string Subject + public string? Subject { - get => (string)GetValue(SubjectProperty); + get => (string?)GetValue(SubjectProperty); set => SetValue(SubjectProperty, value); } + /// + /// Backing BindableProperty for the property. + /// + public static readonly BindableProperty SubjectBackgroundColorProperty = + BindableProperty.Create(nameof(SubjectBackgroundColor), typeof(Color), typeof(Shield), Color.Default, + propertyChanged: OnSubjectBackgroundColorChanged); + + static void OnSubjectBackgroundColorChanged(BindableObject bindable, object oldValue, object newValue) => ((Shield)bindable).UpdateSubjectColor(); + + /// + /// Background of the left side of the . This is a bindable property. + /// + public Color SubjectBackgroundColor + { + get => (Color)GetValue(SubjectBackgroundColorProperty); + set => SetValue(SubjectBackgroundColorProperty, value); + } + + [Obsolete("TextColor is obsolete. Please use StatusTextColor instead")] + [EditorBrowsable(EditorBrowsableState.Never)] + public static readonly BindableProperty TextColorProperty = + BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(Shield), Color.Default, + propertyChanged: OnTextColorChanged); + + public Color TextColor + { + get => (Color)GetValue(TextColorProperty); + set => SetValue(TextColorProperty, value); + } + + static void OnTextColorChanged(BindableObject bindable, object oldValue, object newValue) => ((Shield)bindable).StatusTextColor = (Color)newValue; + + /// + /// Backing BindableProperty for the property. + /// + public static readonly BindableProperty SubjectTextColorProperty = + BindableProperty.Create(nameof(SubjectTextColor), typeof(Color), typeof(Shield), Color.Default, + propertyChanged: OnSubjectTextColorChanged); + + static void OnSubjectTextColorChanged(BindableObject bindable, object oldValue, object newValue) => ((Shield)bindable).UpdateSubjectTextColor(); + + /// + /// Text of the text on the right side of the Shield + /// + public Color SubjectTextColor + { + get => (Color)GetValue(SubjectTextColorProperty); + set => SetValue(SubjectTextColorProperty, value); + } + /// /// Backing BindableProperty for the property. /// @@ -41,43 +92,60 @@ public string Subject /// /// Text that is shown on the right side of the . This is a bindable property. /// - public string Status + public string? Status { - get => (string)GetValue(StatusProperty); + get => (string?)GetValue(StatusProperty); set => SetValue(StatusProperty, value); } - /// - /// Backing BindableProperty for the property. - /// + [Obsolete("Color is obsolete. Please use StatusBackgroundColor instead")] + [EditorBrowsable(EditorBrowsableState.Never)] public static readonly BindableProperty ColorProperty = BindableProperty.Create(nameof(Color), typeof(Color), typeof(Shield), Color.Default, - propertyChanged: OnColorChanged); + propertyChanged: OnColorChanged); static void OnColorChanged(BindableObject bindable, object oldValue, object newValue) => ((Shield)bindable).UpdateColor(); - /// - /// Background of the right side of the . This is a bindable property. - /// public Color Color { get => (Color)GetValue(ColorProperty); set => SetValue(ColorProperty, value); } - public static readonly BindableProperty TextColorProperty = - BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(Shield), Color.Default, - propertyChanged: OnTextColorChanged); + /// + /// Backing BindableProperty for the property. + /// + public static readonly BindableProperty StatusBackgroundColorProperty = + BindableProperty.Create(nameof(StatusBackgroundColor), typeof(Color), typeof(Shield), Color.Default, + propertyChanged: OnStatusBackgroundColorChanged); + + static void OnStatusBackgroundColorChanged(BindableObject bindable, object oldValue, object newValue) => ((Shield)bindable).UpdateStatusBackgroundColor(); + + /// + /// Background of the right side of the . This is a bindable property. + /// + public Color StatusBackgroundColor + { + get => (Color)GetValue(StatusBackgroundColorProperty); + set => SetValue(StatusBackgroundColorProperty, value); + } + + /// + /// Backing BindableProperty for the property. + /// + public static readonly BindableProperty StatusTextColorProperty = + BindableProperty.Create(nameof(StatusTextColor), typeof(Color), typeof(Shield), Color.Default, + propertyChanged: OnStatusTextColorChanged); - static void OnTextColorChanged(BindableObject bindable, object oldValue, object newValue) => ((Shield)bindable).UpdateTextColor(); + static void OnStatusTextColorChanged(BindableObject bindable, object oldValue, object newValue) => ((Shield)bindable).UpdateStatusTextColor(); /// /// Text of the text on the right side of the Shield /// - public Color TextColor + public Color StatusTextColor { - get => (Color)GetValue(TextColorProperty); - set => SetValue(TextColorProperty, value); + get => (Color)GetValue(StatusTextColorProperty); + set => SetValue(StatusTextColorProperty, value); } /// @@ -92,7 +160,7 @@ public Color TextColor /// /// Font size of all the text on the . values can be used. This is a bindable preoprty. /// - [TypeConverter(typeof(FontSizeConverter))] + [Xamarin.Forms.TypeConverter(typeof(FontSizeConverter))] public double FontSize { get => (double)GetValue(FontSizeProperty); @@ -140,9 +208,9 @@ public FontAttributes FontAttributes /// /// Command that is triggered when the is tapped. This is a bindable property. /// - public ICommand Command + public ICommand? Command { - get => (ICommand)GetValue(CommandProperty); + get => (ICommand?)GetValue(CommandProperty); set => SetValue(CommandProperty, value); } @@ -155,7 +223,7 @@ public ICommand Command /// /// Parameter that is provided to the when the is tapped. This is a bindable property. /// - public object CommandParameter + public object? CommandParameter { get => GetValue(CommandParameterProperty); set => SetValue(CommandParameterProperty, value); @@ -164,7 +232,7 @@ public object CommandParameter /// /// Event that is triggered when the is tapped. This is a bindable property. /// - public event EventHandler Tapped; + public event EventHandler? Tapped; Grid ShieldSubjectContainer { get; } = CreateSubjectContainerElement(); @@ -174,6 +242,30 @@ public object CommandParameter Label ShieldStatus { get; } = CreateStatusElement(); + static Grid CreateSubjectContainerElement() + => new Grid() + { + BackgroundColor = Color.FromHex("#555555") + }; + + static Label CreateSubjectElement() + => new Label + { + TextColor = Color.White, + VerticalOptions = LayoutOptions.Center, + Margin = new Thickness(4, 0) + }; + + static Grid CreateStatusContainerElement() + => new Grid(); + + static Label CreateStatusElement() + => new Label + { + VerticalOptions = LayoutOptions.Center, + Margin = new Thickness(4, 0) + }; + protected override void OnControlInitialized(Frame control) { control.CornerRadius = 4; @@ -204,31 +296,7 @@ protected override void OnControlInitialized(Frame control) UpdateIsEnabled(); } - static Grid CreateSubjectContainerElement() - => new Grid - { - BackgroundColor = Color.FromHex("#555555") - }; - - static Label CreateSubjectElement() - => new Label - { - TextColor = Color.White, - VerticalOptions = LayoutOptions.Center, - Margin = new Thickness(4, 0) - }; - - static Grid CreateStatusContainerElement() - => new Grid(); - - static Label CreateStatusElement() - => new Label - { - VerticalOptions = LayoutOptions.Center, - Margin = new Thickness(4, 0) - }; - - protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) + protected override void OnPropertyChanged([CallerMemberName] string propertyName = "") { base.OnPropertyChanged(propertyName); @@ -240,9 +308,15 @@ protected override void OnPropertyChanged([CallerMemberName] string propertyName void UpdateStatus() => ShieldStatus.Text = Status; + void UpdateSubjectColor() => ShieldSubjectContainer.BackgroundColor = SubjectBackgroundColor; + void UpdateColor() => ShieldStatusContainer.BackgroundColor = Color; - void UpdateTextColor() => ShieldStatus.TextColor = TextColor; + void UpdateStatusBackgroundColor() => ShieldStatusContainer.BackgroundColor = StatusBackgroundColor; + + void UpdateSubjectTextColor() => ShieldSubject.TextColor = SubjectTextColor; + + void UpdateStatusTextColor() => ShieldStatus.TextColor = StatusTextColor; void UpdateFont() { @@ -268,11 +342,11 @@ void UpdateIsEnabled() GestureRecognizers.Clear(); } - void OnCloseButtonTapped(object sender, EventArgs e) + void OnCloseButtonTapped(object? sender, EventArgs e) { Tapped?.Invoke(this, EventArgs.Empty); Command?.Execute(CommandParameter); } } } -} \ No newline at end of file +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/SideMenuView/SideMenuView.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/SideMenuView/SideMenuView.shared.cs index b4f817b4c..bbea9fab8 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/SideMenuView/SideMenuView.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/SideMenuView/SideMenuView.shared.cs @@ -38,17 +38,7 @@ public class SideMenuView : BaseTemplatedView readonly SideMenuElementCollection children = new SideMenuElementCollection(); - View overlayView; - - View mainView; - - View leftMenu; - - View rightMenu; - - View activeMenu; - - View inactiveMenu; + View? overlayView, mainView, leftMenu, rightMenu, activeMenu, inactiveMenu; double zeroShift; @@ -87,12 +77,15 @@ public class SideMenuView : BaseTemplatedView public static readonly BindableProperty MenuGestureEnabledProperty = BindableProperty.CreateAttached(nameof(GetMenuGestureEnabled), typeof(bool), typeof(SideMenuView), true); + public static readonly BindableProperty MainViewScaleFactorProperty + = BindableProperty.CreateAttached(nameof(GetMainViewScaleFactor), typeof(double), typeof(SideMenuView), 1.0); + public SideMenuView() { #region Required work-around to prevent linker from removing the platform-specific implementation #if __ANDROID__ if (System.DateTime.Now.Ticks < 0) - _ = new Xamarin.CommunityToolkit.Android.UI.Views.SideMenuViewRenderer(null); + _ = new Xamarin.CommunityToolkit.Android.UI.Views.SideMenuViewRenderer(ToolkitPlatform.Context ?? throw new NullReferenceException()); #elif __IOS__ if (System.DateTime.Now.Ticks < 0) _ = new Xamarin.CommunityToolkit.iOS.UI.Views.SideMenuViewRenderer(); @@ -157,7 +150,13 @@ public static bool GetMenuGestureEnabled(BindableObject bindable) public static void SetMenuGestureEnabled(BindableObject bindable, bool value) => bindable.SetValue(MenuGestureEnabledProperty, value); - internal void OnPanUpdated(object sender, PanUpdatedEventArgs e) + public static double GetMainViewScaleFactor(BindableObject bindable) + => (double)bindable.GetValue(MainViewScaleFactorProperty); + + public static void SetMainViewScaleFactor(BindableObject bindable, double value) + => bindable.SetValue(MainViewScaleFactorProperty, value); + + internal void OnPanUpdated(object? sender, PanUpdatedEventArgs e) { var shift = e.TotalX; var verticalShift = e.TotalY; @@ -344,7 +343,10 @@ void PerformUpdate(bool isAnimated = true) } void SetOverlayViewInputTransparent(SideMenuState state) - => overlayView.InputTransparent = state == SideMenuState.MainViewShown; + { + _ = overlayView ?? throw new NullReferenceException(); + overlayView.InputTransparent = state == SideMenuState.MainViewShown; + } SideMenuState ResolveSwipeState(bool isRightSwipe) { @@ -364,26 +366,38 @@ SideMenuState ResolveSwipeState(bool isRightSwipe) return isRightSwipe ? left : right; } - bool TryUpdateShift(double sift, bool shouldUpdatePreviousShift, bool shouldCheckMenuGestureEnabled) + bool TryUpdateShift(double shift, bool shouldUpdatePreviousShift, bool shouldCheckMenuGestureEnabled) { - SetActiveView(sift >= 0); + SetActiveView(shift >= 0); if (activeMenu == null) return false; if (shouldCheckMenuGestureEnabled && !GetMenuGestureEnabled(activeMenu)) return false; - sift = Sign(sift) * Min(Abs(sift), activeMenu.Width); - if (Abs(Shift - sift) <= double.Epsilon) + _ = mainView ?? throw new NullReferenceException(); + + var activeMenuWidth = activeMenu.Width; + var mainViewWidth = mainView.Width; + + shift = Sign(shift) * Min(Abs(shift), activeMenuWidth); + if (Abs(Shift - shift) <= double.Epsilon) return false; - Shift = sift; - SetCurrentGestureState(sift); + Shift = shift; + SetCurrentGestureState(shift); if (shouldUpdatePreviousShift) - previousShift = sift; + previousShift = shift; + + _ = overlayView ?? throw new NullReferenceException(); - mainView.TranslationX = sift; - overlayView.TranslationX = sift; + using (mainView.Batch()) + { + var scale = 1 - ((1 - GetMainViewScaleFactor(activeMenu)) * animationEasing.Ease(shift / activeMenuWidth)); + mainView.Scale = scale; + mainView.TranslationX = shift - (Sign(shift) * mainViewWidth * 0.5 * (1 - scale)); + } + overlayView.TranslationX = shift; return true; } @@ -439,7 +453,7 @@ void SetActiveView(bool isLeft) if (inactiveMenu == null || activeMenu == null || - leftMenu.X + leftMenu.Width <= rightMenu.X || + leftMenu?.X + leftMenu?.Width <= rightMenu?.X || Control.Children.IndexOf(inactiveMenu) < Control.Children.IndexOf(activeMenu)) return; @@ -511,7 +525,7 @@ void CleanTimeShiftItems() } } - void OnChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + void OnChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { HandleChildren(e.OldItems, RemoveChild); HandleChildren(e.NewItems, AddChild); @@ -522,7 +536,10 @@ void HandleChildren(IList items, Action action) if (items != null) { foreach (var item in items) - action?.Invoke((View)item); + { + if (item != null && action != null) + action((View)item); + } } } @@ -565,7 +582,7 @@ void RemoveChild(View view) inactiveMenu = null; } - void OnLayoutChanged(object sender, EventArgs e) + void OnLayoutChanged(object? sender, EventArgs e) { if (mainView == null) return; @@ -574,7 +591,7 @@ void OnLayoutChanged(object sender, EventArgs e) Control.RaiseChild(overlayView); } - bool CheckMenuGestureEnabled(View menuView) + bool CheckMenuGestureEnabled(View? menuView) => menuView != null && GetMenuGestureEnabled(menuView); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/SideMenuView/SideMenuViewRenderer.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/SideMenuView/SideMenuViewRenderer.android.cs index 1b4649fbe..39151aaa1 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/SideMenuView/SideMenuViewRenderer.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/SideMenuView/SideMenuViewRenderer.android.cs @@ -31,7 +31,7 @@ public class SideMenuViewRenderer : VisualElementRenderer public SideMenuViewRenderer(Context context) : base(context) - => density = context.Resources.DisplayMetrics.Density; + => density = context.Resources?.DisplayMetrics?.Density ?? throw new NullReferenceException(); double GestureThreshold => Element.GestureThreshold >= 0 ? Element.GestureThreshold diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/SideMenuView/SideMenuViewRenderer.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/SideMenuView/SideMenuViewRenderer.ios.cs index 9da312c3f..795dc4776 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/SideMenuView/SideMenuViewRenderer.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/SideMenuView/SideMenuViewRenderer.ios.cs @@ -15,9 +15,9 @@ public class SideMenuViewRenderer : VisualElementRenderer { const double defaultGestureThreshold = 7.0; - UISwipeGestureRecognizer leftSwipeGestureRecognizer; + UISwipeGestureRecognizer? leftSwipeGestureRecognizer; - UISwipeGestureRecognizer rightSwipeGestureRecognizer; + UISwipeGestureRecognizer? rightSwipeGestureRecognizer; public SideMenuViewRenderer() { @@ -61,7 +61,7 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - void Dispose(ref UISwipeGestureRecognizer gestureRecognizer) + void Dispose(ref UISwipeGestureRecognizer? gestureRecognizer) { if (gestureRecognizer != null) { @@ -85,7 +85,7 @@ bool ShouldBeRequiredToFailBy(UIGestureRecognizer gestureRecognizer, UIGestureRe bool ShouldRecognizeSimultaneously(UIGestureRecognizer gestureRecognizer, UIGestureRecognizer otherGestureRecognizer) { - if (!(gestureRecognizer is UIPanGestureRecognizer panGesture)) + if (gestureRecognizer is not UIPanGestureRecognizer panGesture) return true; var parent = Element?.Parent; diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/NativeSnackButton.ios.macos.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/NativeSnackButton.ios.macos.cs index 5edc40451..06ba6da0c 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/NativeSnackButton.ios.macos.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/NativeSnackButton.ios.macos.cs @@ -2,29 +2,76 @@ using System.Threading.Tasks; #if __IOS__ using Xamarin.CommunityToolkit.UI.Views.Helpers.iOS; +using UIKit; #elif __MACOS__ +using CoreGraphics; using Xamarin.CommunityToolkit.UI.Views.Helpers.macOS; +using AppKit; #endif namespace Xamarin.CommunityToolkit.Views.Snackbar.Helpers { - class NativeActionButton +#if __IOS__ + class NativeSnackButton : UIButton +#else + class NativeSnackButton : NSButton +#endif { - public Func Action { get; protected set; } + public NativeSnackButton(double left, double top, double right, double bottom) +#if __IOS__ + : base(UIButtonType.System) +#endif + { + Left = left; + Top = top; + Right = right; + Bottom = bottom; + LineBreakMode = NativeSnackButtonAppearance.LineBreakMode; +#if __IOS__ + ContentEdgeInsets = new UIEdgeInsets((nfloat)top, (nfloat)left, (nfloat)bottom, (nfloat)right); + TouchUpInside += async (s, e) => + { + if (SnackButtonAction != null) + await SnackButtonAction(); + }; + } +#else + WantsLayer = true; + Activated += async (s, e) => + { + if (SnackButtonAction != null) + await SnackButtonAction(); + }; + } - public string ActionButtonText { get; protected set; } + public override CGSize IntrinsicContentSize => new CGSize( + base.IntrinsicContentSize.Width + Left + Right, + base.IntrinsicContentSize.Height + Top + Bottom); +#endif + + public double Left { get; } + + public double Top { get; } + + public double Right { get; } - public NativeSnackButtonAppearance Appearance { get; protected set; } = new NativeSnackButtonAppearance(); + public double Bottom { get; } - public NativeActionButton SetAction(Func action) + public Func? SnackButtonAction { get; protected set; } + + public NativeSnackButton SetAction(Func action) { - Action = action; + SnackButtonAction = action; return this; } - public NativeActionButton SetActionButtonText(string title) + public NativeSnackButton SetActionButtonText(string title) { - ActionButtonText = title; +#if __IOS__ + SetTitle(title, UIControlState.Normal); +#else + Title = title; +#endif return this; } } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/PaddedLabel.ios.macos.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/PaddedLabel.ios.macos.cs new file mode 100644 index 000000000..6b395ccfd --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/PaddedLabel.ios.macos.cs @@ -0,0 +1,45 @@ +using System; +using CoreGraphics; +#if __IOS__ +using UIKit; +#else +using AppKit; +#endif + +namespace Xamarin.CommunityToolkit.UI.Views.Helpers +{ +#if __IOS__ + class PaddedLabel : UILabel +#else + class PaddedLabel : NSTextField +#endif + { + public PaddedLabel(nfloat left, nfloat top, nfloat right, nfloat bottom) + { + Left = left; + Top = top; + Right = right; + Bottom = bottom; + } + + public nfloat Left { get; } + + public nfloat Top { get; } + + public nfloat Right { get; } + + public nfloat Bottom { get; } + + public override CGSize IntrinsicContentSize => new CGSize( + base.IntrinsicContentSize.Width + Left + Right, + base.IntrinsicContentSize.Height + Top + Bottom); + +#if __IOS__ + public override void DrawText(CGRect rect) + { + var insets = new UIEdgeInsets(Top, Left, Bottom, Right); + base.DrawText(insets.InsetRect(rect)); + } +#endif + } +} \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/SnackBarLayout.ios.macos.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/SnackBarLayout.ios.macos.cs index fabf709af..e235db872 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/SnackBarLayout.ios.macos.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/SnackBarLayout.ios.macos.cs @@ -6,21 +6,19 @@ class SnackBarLayout { public nfloat MarginBottom { get; set; } = 10.0f; - public nfloat MarginCenter { get; set; } = 0.0f; - - public nfloat MarginLeading { get; set; } = 10.0f; + public nfloat MarginLeft { get; set; } = 10.0f; public nfloat MarginTop { get; set; } = 10.0f; - public nfloat MarginTrailing { get; set; } = 10.0f; + public nfloat MarginRight { get; set; } = 10.0f; public nfloat PaddingBottom { get; set; } = 10.0f; - public nfloat PaddingLeading { get; set; } = 10.0f; + public nfloat PaddingLeft { get; set; } = 10.0f; public nfloat PaddingTop { get; set; } = 10.0f; - public nfloat PaddingTrailing { get; set; } = 10.0f; + public nfloat PaddingRight { get; set; } = 10.0f; public nfloat Spacing { get; set; } = 10.0f; } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/SnackBarLayout.uwp.wpf.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/SnackBarLayout.uwp.wpf.cs index 1c4ca760a..1d6c43371 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/SnackBarLayout.uwp.wpf.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/SnackBarLayout.uwp.wpf.cs @@ -33,13 +33,25 @@ public SnackBarLayout(SnackBarOptions options) #else var messageLabel = new Label { - Content = options.MessageOptions.Message, + Content = options.MessageOptions.Message }; #endif + messageLabel.Padding = new Thickness(options.MessageOptions.Padding.Left, + options.MessageOptions.Padding.Top, + options.MessageOptions.Padding.Right, + options.MessageOptions.Padding.Bottom); + if (options.MessageOptions.Font != Forms.Font.Default) { - messageLabel.FontSize = options.MessageOptions.Font.FontSize; - messageLabel.FontFamily = new FontFamily(options.MessageOptions.Font.FontFamily); + if (options.MessageOptions.Font.FontSize > 0) + { + messageLabel.FontSize = options.MessageOptions.Font.FontSize; + } + + if (options.MessageOptions.Font.FontFamily != null) + { + messageLabel.FontFamily = new FontFamily(options.MessageOptions.Font.FontFamily); + } } if (options.MessageOptions.Foreground != Forms.Color.Default) @@ -60,8 +72,13 @@ public SnackBarLayout(SnackBarOptions options) Command = new Forms.Command(async () => { OnSnackBarActionExecuted?.Invoke(); - await action.Action(); - }) + if (action.Action != null) + await action.Action(); + }), + Padding = new Thickness(action.Padding.Left, + action.Padding.Top, + action.Padding.Right, + action.Padding.Bottom) }; if (action.Font != Forms.Font.Default) { @@ -85,6 +102,6 @@ public SnackBarLayout(SnackBarOptions options) } } - public Action OnSnackBarActionExecuted; + public Action? OnSnackBarActionExecuted; } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/IOSSnackBar.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/IOSSnackBar.ios.cs index 2d5f1ae75..9a228aaee 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/IOSSnackBar.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/IOSSnackBar.ios.cs @@ -12,11 +12,11 @@ namespace Xamarin.CommunityToolkit.UI.Views.Helpers.iOS { class NativeSnackBar { - NSTimer timer; + NSTimer? timer; - public List Actions { get; protected set; } = new List(); + public List Actions { get; protected set; } = new List(); - public Func TimeoutAction { get; protected set; } + public Func? TimeoutAction { get; protected set; } public NativeSnackBarAppearance Appearance { get; protected set; } = new NativeSnackBarAppearance(); @@ -24,11 +24,11 @@ class NativeSnackBar public SnackBarLayout Layout { get; } = new SnackBarLayout(); - public string Message { get; protected set; } + public string Message { get; protected set; } = string.Empty; - public UIViewController ParentController { get; protected set; } + public UIViewController? ParentController { get; protected set; } - protected BaseSnackBarView SnackBarView { get; set; } + protected BaseSnackBarView? SnackBarView { get; set; } public void Dismiss() { @@ -70,14 +70,15 @@ public NativeSnackBar Show() { SnackBarView = GetSnackBarView(); - SnackBarView.ParentView.AddSubview(SnackBarView); - SnackBarView.ParentView.BringSubviewToFront(SnackBarView); + SnackBarView.ParentView?.AddSubview(SnackBarView); + SnackBarView.ParentView?.BringSubviewToFront(SnackBarView); SnackBarView.Setup(); timer = NSTimer.CreateScheduledTimer(TimeSpan.FromMilliseconds(Duration), async t => { - await TimeoutAction(); + if (TimeoutAction != null) + await TimeoutAction(); Dismiss(); }); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackBarAppearance.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackBarAppearance.ios.cs index eada39180..21ec815ec 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackBarAppearance.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackBarAppearance.ios.cs @@ -19,15 +19,9 @@ class NativeSnackBarAppearance public static UIFont DefaultFont { get; } = Forms.Font.Default.ToUIFont(); } - class NativeSnackButtonAppearance + static class NativeSnackButtonAppearance { - public UIColor Background { get; set; } = DefaultColor; - - public UIColor Foreground { get; set; } = DefaultColor; - - public UIFont Font { get; set; } = DefaultFont; - - public UILineBreakMode LineBreakMode { get; set; } = UILineBreakMode.MiddleTruncation; + public static UILineBreakMode LineBreakMode { get; set; } = UILineBreakMode.MiddleTruncation; public static UIColor DefaultColor { get; } = Forms.Color.Default.ToUIColor(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackbarViews/ActionMessageSnackBarView.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackbarViews/ActionMessageSnackBarView.ios.cs index 74ec04566..6cea42669 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackbarViews/ActionMessageSnackBarView.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackbarViews/ActionMessageSnackBarView.ios.cs @@ -1,5 +1,4 @@ using System; -using UIKit; using Xamarin.CommunityToolkit.UI.Views.Helpers.iOS.SnackBarViews; namespace Xamarin.CommunityToolkit.UI.Views.Helpers.iOS @@ -11,39 +10,14 @@ public ActionMessageSnackBarView(NativeSnackBar snackBar) { } - // Gets the maximum width of the action button. Possible values 0 to 1. - protected virtual nfloat ActionButtonMaxWidth => 1f; - protected override void Initialize() { base.Initialize(); - foreach (var action in SnackBar.Actions) - { - var actionButton = new UIButton(UIButtonType.System); - if (action.Appearance.Background != NativeSnackButtonAppearance.DefaultColor) - { - actionButton.BackgroundColor = action.Appearance.Background; - } - - if (action.Appearance.Foreground != NativeSnackButtonAppearance.DefaultColor) - { - actionButton.SetTitleColor(action.Appearance.Foreground, UIControlState.Normal); - } - - if (action.Appearance.Font != NativeSnackButtonAppearance.DefaultFont) - { - actionButton.Font = action.Appearance.Font; - } - - actionButton.SetTitle(action.ActionButtonText, UIControlState.Normal); - actionButton.TitleLabel.LineBreakMode = action.Appearance.LineBreakMode; - actionButton.TouchUpInside += async (s, e) => - { - await action.Action(); - Dismiss(); - }; + _ = StackView ?? throw new NullReferenceException(); + foreach (var actionButton in SnackBar.Actions) + { StackView.AddArrangedSubview(actionButton); } } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackbarViews/BaseSnackBarView.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackbarViews/BaseSnackBarView.ios.cs index c3079c15a..6ab109362 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackbarViews/BaseSnackBarView.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackbarViews/BaseSnackBarView.ios.cs @@ -1,4 +1,5 @@ -using UIKit; +using System; +using UIKit; using Xamarin.CommunityToolkit.UI.Views.Helpers.iOS.Extensions; namespace Xamarin.CommunityToolkit.UI.Views.Helpers.iOS.SnackBar @@ -7,13 +8,13 @@ abstract class BaseSnackBarView : UIView { public BaseSnackBarView(NativeSnackBar snackBar) => SnackBar = snackBar; - public virtual UIView ParentView => SnackBar.ParentController != null + public virtual UIView? ParentView => SnackBar.ParentController != null ? SnackBar.ParentController.View : UIApplication.SharedApplication.KeyWindow; protected NativeSnackBar SnackBar { get; } - protected UIStackView StackView { get; set; } + protected UIStackView? StackView { get; set; } public virtual void Dismiss() => RemoveFromSuperview(); @@ -30,14 +31,17 @@ protected virtual void ConstrainChildren() protected virtual void ConstrainInParent() { + _ = StackView ?? throw new InvalidOperationException("BaseSnackBarView.Initialize() not called"); + _ = ParentView ?? throw new System.NullReferenceException(); + this.SafeBottomAnchor().ConstraintEqualTo(GetBottomAnchor(), -SnackBar.Layout.MarginBottom).Active = true; this.SafeTopAnchor().ConstraintGreaterThanOrEqualTo(GetTopAnchor(), SnackBar.Layout.MarginTop).Active = true; - this.SafeLeadingAnchor().ConstraintGreaterThanOrEqualTo(ParentView.SafeLeadingAnchor(), SnackBar.Layout.MarginLeading).Active = true; - this.SafeTrailingAnchor().ConstraintLessThanOrEqualTo(ParentView.SafeTrailingAnchor(), -SnackBar.Layout.MarginTrailing).Active = true; + this.SafeLeadingAnchor().ConstraintGreaterThanOrEqualTo(ParentView.SafeLeadingAnchor(), SnackBar.Layout.MarginLeft).Active = true; + this.SafeTrailingAnchor().ConstraintLessThanOrEqualTo(ParentView.SafeTrailingAnchor(), -SnackBar.Layout.MarginRight).Active = true; this.SafeCenterXAnchor().ConstraintEqualTo(ParentView.SafeCenterXAnchor()).Active = true; - StackView.SafeLeadingAnchor().ConstraintEqualTo(this.SafeLeadingAnchor(), SnackBar.Layout.PaddingLeading).Active = true; - StackView.SafeTrailingAnchor().ConstraintEqualTo(this.SafeTrailingAnchor(), -SnackBar.Layout.PaddingTrailing).Active = true; + StackView.SafeLeadingAnchor().ConstraintEqualTo(this.SafeLeadingAnchor(), SnackBar.Layout.PaddingLeft).Active = true; + StackView.SafeTrailingAnchor().ConstraintEqualTo(this.SafeTrailingAnchor(), -SnackBar.Layout.PaddingRight).Active = true; StackView.SafeBottomAnchor().ConstraintEqualTo(this.SafeBottomAnchor(), -SnackBar.Layout.PaddingBottom).Active = true; StackView.SafeTopAnchor().ConstraintEqualTo(this.SafeTopAnchor(), SnackBar.Layout.PaddingTop).Active = true; } @@ -46,6 +50,7 @@ protected virtual NSLayoutYAxisAnchor GetBottomAnchor() { if (UIDevice.CurrentDevice.CheckSystemVersion(11, 0) || SnackBar.ParentController == null) { + _ = ParentView ?? throw new System.NullReferenceException(); return ParentView.SafeBottomAnchor(); } @@ -56,16 +61,18 @@ protected virtual NSLayoutYAxisAnchor GetCenterYAnchor() { if (UIDevice.CurrentDevice.CheckSystemVersion(11, 0) || SnackBar.ParentController == null) { + _ = ParentView ?? throw new NullReferenceException(); return ParentView.SafeCenterYAnchor(); } - return SnackBar.ParentController.View?.CenterYAnchor; + return SnackBar.ParentController.View?.CenterYAnchor ?? throw new NullReferenceException(); } protected virtual NSLayoutYAxisAnchor GetTopAnchor() { if (UIDevice.CurrentDevice.CheckSystemVersion(11, 0) || SnackBar.ParentController == null) { + _ = ParentView ?? throw new NullReferenceException(); return ParentView.SafeTopAnchor(); } @@ -78,6 +85,7 @@ protected virtual void Initialize() AddSubview(StackView); StackView.Axis = UILayoutConstraintAxis.Horizontal; StackView.TranslatesAutoresizingMaskIntoConstraints = false; + StackView.Spacing = SnackBar.Layout.Spacing; if (SnackBar.Appearance.Background != NativeSnackBarAppearance.DefaultColor) { StackView.BackgroundColor = SnackBar.Appearance.Background; @@ -86,4 +94,4 @@ protected virtual void Initialize() TranslatesAutoresizingMaskIntoConstraints = false; } } -} \ No newline at end of file +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackbarViews/MessageSnackBarView.ios.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackbarViews/MessageSnackBarView.ios.cs index 6b4451455..fadf61a57 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackbarViews/MessageSnackBarView.ios.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/iOS/SnackbarViews/MessageSnackBarView.ios.cs @@ -1,4 +1,4 @@ -using UIKit; +using System; using Xamarin.CommunityToolkit.UI.Views.Helpers.iOS.SnackBar; namespace Xamarin.CommunityToolkit.UI.Views.Helpers.iOS.SnackBarViews @@ -14,7 +14,10 @@ protected override void Initialize() { base.Initialize(); - var messageLabel = new UILabel + var messageLabel = new PaddedLabel(SnackBar.Layout.PaddingLeft, + SnackBar.Layout.PaddingTop, + SnackBar.Layout.PaddingRight, + SnackBar.Layout.PaddingBottom) { Text = SnackBar.Message, Lines = 0, @@ -36,6 +39,7 @@ protected override void Initialize() messageLabel.Font = SnackBar.Appearance.Font; } + _ = StackView ?? throw new NullReferenceException(); StackView.AddArrangedSubview(messageLabel); } } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/MacOSSnackBar.macos.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/MacOSSnackBar.macos.cs index 35059b192..28763162c 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/MacOSSnackBar.macos.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/MacOSSnackBar.macos.cs @@ -10,11 +10,11 @@ namespace Xamarin.CommunityToolkit.UI.Views.Helpers.macOS { class NativeSnackBar { - NSTimer timer; + NSTimer? timer; - public Func TimeoutAction { get; protected set; } + public Func? TimeoutAction { get; protected set; } - public List Actions { get; protected set; } = new List(); + public List Actions { get; protected set; } = new List(); public NativeSnackBarAppearance Appearance { get; protected set; } = new NativeSnackBarAppearance(); @@ -22,9 +22,9 @@ class NativeSnackBar public SnackBarLayout Layout { get; } = new SnackBarLayout(); - public string Message { get; protected set; } + public string Message { get; protected set; } = string.Empty; - protected BaseSnackBarView SnackBarView { get; set; } + protected BaseSnackBarView? SnackBarView { get; set; } public void Dismiss() { @@ -66,7 +66,8 @@ public NativeSnackBar Show() timer = NSTimer.CreateScheduledTimer(TimeSpan.FromMilliseconds(Duration), async t => { - await TimeoutAction(); + if (TimeoutAction != null) + await TimeoutAction(); Dismiss(); }); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/SnackBarAppearance.macos.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/SnackBarAppearance.macos.cs index 124804642..50148fb6a 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/SnackBarAppearance.macos.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/SnackBarAppearance.macos.cs @@ -1,4 +1,5 @@ using AppKit; +using CoreGraphics; using Xamarin.Forms.Platform.MacOS; namespace Xamarin.CommunityToolkit.UI.Views.Helpers.macOS @@ -11,22 +12,20 @@ class NativeSnackBarAppearance public NSFont Font { get; set; } = DefaultFont; + public CGRect Padding { get; set; } = DefaultPadding; + public NSTextAlignment TextAlignment { get; set; } = NSTextAlignment.Left; public static NSColor DefaultColor { get; } = Forms.Color.Default.ToNSColor(); public static NSFont DefaultFont { get; } = Forms.Font.Default.ToNSFont(); + + public static CGRect DefaultPadding { get; } = new CGRect(0, 0, 0, 0); } - class NativeSnackButtonAppearance + static class NativeSnackButtonAppearance { - public NSColor Background { get; set; } = DefaultColor; - - public NSColor Foreground { get; set; } = DefaultColor; - - public NSFont Font { get; set; } = DefaultFont; - - public NSLineBreakMode LineBreakMode { get; set; } = NSLineBreakMode.TruncatingMiddle; + public static NSLineBreakMode LineBreakMode { get; set; } = NSLineBreakMode.TruncatingMiddle; public static NSColor DefaultColor { get; } = Forms.Color.Default.ToNSColor(); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/SnackbarViews/ActionMessageSnackBarView.macos.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/SnackbarViews/ActionMessageSnackBarView.macos.cs index 71f9b4802..00f646dc1 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/SnackbarViews/ActionMessageSnackBarView.macos.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/SnackbarViews/ActionMessageSnackBarView.macos.cs @@ -1,7 +1,4 @@ -using System; -using AppKit; - -namespace Xamarin.CommunityToolkit.UI.Views.Helpers.macOS.SnackBarViews +namespace Xamarin.CommunityToolkit.UI.Views.Helpers.macOS.SnackBarViews { class ActionMessageSnackBarView : MessageSnackBarView { @@ -10,36 +7,12 @@ public ActionMessageSnackBarView(NativeSnackBar snackBar) { } - // Gets the maximum width of the action button. Possible values 0 to 1. - protected virtual nfloat ActionButtonMaxWidth => 1f; - protected override void Initialize() { base.Initialize(); - foreach (var action in SnackBar.Actions) + foreach (var actionButton in SnackBar.Actions) { - var actionButton = new NSButton - { - Title = action.ActionButtonText, - WantsLayer = true, - LineBreakMode = action.Appearance.LineBreakMode, - }; - if (SnackBar.Appearance.Background != NativeSnackButtonAppearance.DefaultColor) - { - actionButton.Layer.BackgroundColor = action.Appearance.Background.CGColor; - } - - if (SnackBar.Appearance.Font != NativeSnackButtonAppearance.DefaultFont) - { - actionButton.Font = action.Appearance.Font; - } - - actionButton.Activated += async (s, e) => - { - await action.Action(); - Dismiss(); - }; - StackView.AddArrangedSubview(actionButton); + StackView?.AddArrangedSubview(actionButton); } } } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/SnackbarViews/BaseSnackBarView.macos.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/SnackbarViews/BaseSnackBarView.macos.cs index 941d78de0..1217526a7 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/SnackbarViews/BaseSnackBarView.macos.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/SnackbarViews/BaseSnackBarView.macos.cs @@ -10,7 +10,7 @@ abstract class BaseSnackBarView : NSView protected NativeSnackBar SnackBar { get; } - public NSStackView StackView { get; set; } + public NSStackView? StackView { get; set; } public virtual void Dismiss() => RemoveFromSuperview(); @@ -29,29 +29,36 @@ protected virtual void ConstrainInParent() { BottomAnchor.ConstraintEqualToAnchor(ParentView.BottomAnchor, -SnackBar.Layout.MarginBottom).Active = true; TopAnchor.ConstraintGreaterThanOrEqualToAnchor(ParentView.TopAnchor, SnackBar.Layout.MarginTop).Active = true; - LeadingAnchor.ConstraintGreaterThanOrEqualToAnchor(ParentView.LeadingAnchor, SnackBar.Layout.MarginLeading).Active = true; - TrailingAnchor.ConstraintGreaterThanOrEqualToAnchor(ParentView.TrailingAnchor, -SnackBar.Layout.MarginTrailing).Active = true; + LeadingAnchor.ConstraintGreaterThanOrEqualToAnchor(ParentView.LeadingAnchor, SnackBar.Layout.MarginLeft).Active = true; + TrailingAnchor.ConstraintGreaterThanOrEqualToAnchor(ParentView.TrailingAnchor, -SnackBar.Layout.MarginRight).Active = true; CenterXAnchor.ConstraintEqualToAnchor(ParentView.CenterXAnchor).Active = true; - StackView.LeadingAnchor.ConstraintEqualToAnchor(LeadingAnchor, SnackBar.Layout.PaddingLeading).Active = true; - StackView.TrailingAnchor.ConstraintEqualToAnchor(TrailingAnchor, -SnackBar.Layout.PaddingTrailing).Active = true; - StackView.BottomAnchor.ConstraintEqualToAnchor(BottomAnchor, -SnackBar.Layout.PaddingBottom).Active = true; - StackView.TopAnchor.ConstraintEqualToAnchor(TopAnchor, SnackBar.Layout.PaddingTop).Active = true; + if (StackView != null) + { + StackView.LeadingAnchor.ConstraintEqualToAnchor(LeadingAnchor, SnackBar.Layout.PaddingLeft).Active = true; + StackView.TrailingAnchor.ConstraintEqualToAnchor(TrailingAnchor, -SnackBar.Layout.PaddingRight).Active = true; + StackView.BottomAnchor.ConstraintEqualToAnchor(BottomAnchor, -SnackBar.Layout.PaddingBottom).Active = true; + StackView.TopAnchor.ConstraintEqualToAnchor(TopAnchor, SnackBar.Layout.PaddingTop).Active = true; + } } protected virtual void Initialize() { - StackView = new NSStackView(); - StackView.WantsLayer = true; + StackView = new NSStackView + { + WantsLayer = true + }; + AddSubview(StackView); - if (SnackBar.Appearance.Background != NativeSnackBarAppearance.DefaultColor) + + if (SnackBar.Appearance.Background != NativeSnackBarAppearance.DefaultColor && StackView.Layer != null) { StackView.Layer.BackgroundColor = SnackBar.Appearance.Background.CGColor; } StackView.Orientation = NSUserInterfaceLayoutOrientation.Horizontal; StackView.TranslatesAutoresizingMaskIntoConstraints = false; - StackView.Spacing = 5; + StackView.Spacing = SnackBar.Layout.Spacing; TranslatesAutoresizingMaskIntoConstraints = false; } } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/SnackbarViews/MessageSnackBarView.macos.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/SnackbarViews/MessageSnackBarView.macos.cs index 1c68b60c9..dd470e02c 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/SnackbarViews/MessageSnackBarView.macos.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Helpers/macOS/SnackbarViews/MessageSnackBarView.macos.cs @@ -1,6 +1,4 @@ -using AppKit; - -namespace Xamarin.CommunityToolkit.UI.Views.Helpers.macOS.SnackBarViews +namespace Xamarin.CommunityToolkit.UI.Views.Helpers.macOS.SnackBarViews { class MessageSnackBarView : BaseSnackBarView { @@ -12,7 +10,10 @@ public MessageSnackBarView(NativeSnackBar snackBar) protected override void Initialize() { base.Initialize(); - var messageLabel = new NSTextField + var messageLabel = new PaddedLabel(SnackBar.Layout.PaddingLeft, + SnackBar.Layout.PaddingTop, + SnackBar.Layout.PaddingRight, + SnackBar.Layout.PaddingBottom) { StringValue = SnackBar.Message, Selectable = false, @@ -34,7 +35,7 @@ protected override void Initialize() messageLabel.Font = SnackBar.Appearance.Font; } - StackView.AddArrangedSubview(messageLabel); + StackView?.AddArrangedSubview(messageLabel); } } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Options/MessageOptions.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Options/MessageOptions.shared.cs index 62a588a24..448fdfe98 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Options/MessageOptions.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Options/MessageOptions.shared.cs @@ -9,7 +9,7 @@ public class MessageOptions /// public string Message { get; set; } = DefaultMessage; - public static string DefaultMessage { get; set; } + public static string DefaultMessage { get; set; } = string.Empty; /// /// Gets or sets the font for the SnackBar message. @@ -24,5 +24,12 @@ public class MessageOptions public Color Foreground { get; set; } = DefaultForeground; public static Color DefaultForeground { get; set; } = Color.Default; + + /// + /// Gets or sets the padding for the SnackBar message. + /// + public Thickness Padding { get; set; } + + public static Thickness DefaultPadding { get; set; } = new Thickness(0, 0, 0, 0); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Options/SnackBarActionOptions.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Options/SnackBarActionOptions.shared.cs index a44f7eaf8..59ca5a76f 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Options/SnackBarActionOptions.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/Options/SnackBarActionOptions.shared.cs @@ -9,16 +9,16 @@ public class SnackBarActionOptions /// /// Gets or sets the action for the SnackBar action button. /// - public Func Action { get; set; } = DefaultAction; + public Func? Action { get; set; } = DefaultAction; - public static Func DefaultAction { get; set; } = null; + public static Func? DefaultAction { get; set; } = null; /// /// Gets or sets the text for the SnackBar action button. /// public string Text { get; set; } = DefaultText; - public static string DefaultText { get; set; } + public static string DefaultText { get; set; } = string.Empty; /// /// Gets or sets the font for the SnackBar action button. @@ -40,5 +40,12 @@ public class SnackBarActionOptions public Color ForegroundColor { get; set; } = DefaultForegroundColor; public static Color DefaultForegroundColor { get; set; } = Color.Default; + + /// + /// Gets or sets the padding for the SnackBar message. + /// + public Thickness Padding { get; set; } + + public static Thickness DefaultPadding { get; set; } = new Thickness(0, 0, 0, 0); } } \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.android.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.android.cs index 5eda53d2d..44fd9ac0d 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.android.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.android.cs @@ -4,6 +4,7 @@ using Xamarin.Forms.Platform.Android; using Xamarin.CommunityToolkit.UI.Views.Options; using Android.Util; +using System; #if MONOANDROID10_0 using AndroidSnackBar = Google.Android.Material.Snackbar.Snackbar; #else @@ -24,8 +25,17 @@ internal void Show(Page sender, SnackBarOptions arguments) snackBarView.SetBackgroundColor(arguments.BackgroundColor.ToAndroid()); } - var snackTextView = snackBarView.FindViewById(Resource.Id.snackbar_text); + var snackTextView = snackBarView.FindViewById(Resource.Id.snackbar_text) ?? throw new NullReferenceException(); snackTextView.SetMaxLines(10); + + if (arguments.MessageOptions.Padding != MessageOptions.DefaultPadding) + { + snackBarView.SetPadding((int)arguments.MessageOptions.Padding.Left, + (int)arguments.MessageOptions.Padding.Top, + (int)arguments.MessageOptions.Padding.Right, + (int)arguments.MessageOptions.Padding.Bottom); + } + if (arguments.MessageOptions.Foreground != Forms.Color.Default) { snackTextView.SetTextColor(arguments.MessageOptions.Foreground.ToAndroid()); @@ -33,7 +43,11 @@ internal void Show(Page sender, SnackBarOptions arguments) if (arguments.MessageOptions.Font != Font.Default) { - snackTextView.SetTextSize(ComplexUnitType.Dip, (float)arguments.MessageOptions.Font.FontSize); + if (arguments.MessageOptions.Font.FontSize > 0) + { + snackTextView.SetTextSize(ComplexUnitType.Dip, (float)arguments.MessageOptions.Font.FontSize); + } + snackTextView.SetTypeface(arguments.MessageOptions.Font.ToTypeface(), TypefaceStyle.Normal); } @@ -43,21 +57,37 @@ internal void Show(Page sender, SnackBarOptions arguments) foreach (var action in arguments.Actions) { - snackBar.SetAction(action.Text, async v => await action.Action()); + snackBar.SetAction(action.Text, async v => + { + if (action.Action != null) + await action.Action(); + }); if (action.ForegroundColor != Forms.Color.Default) { snackBar.SetActionTextColor(action.ForegroundColor.ToAndroid()); } - var snackActionButtonView = snackBarView.FindViewById(Resource.Id.snackbar_action); + var snackActionButtonView = snackBarView.FindViewById(Resource.Id.snackbar_action) ?? throw new NullReferenceException(); if (arguments.BackgroundColor != Forms.Color.Default) { snackActionButtonView.SetBackgroundColor(action.BackgroundColor.ToAndroid()); } - if (action.Font != Forms.Font.Default) + if (action.Padding != SnackBarActionOptions.DefaultPadding) + { + snackActionButtonView.SetPadding((int)action.Padding.Left, + (int)action.Padding.Top, + (int)action.Padding.Right, + (int)action.Padding.Bottom); + } + + if (action.Font != Font.Default) { - snackActionButtonView.SetTextSize(ComplexUnitType.Dip, (float)action.Font.FontSize); + if (action.Font.FontSize > 0) + { + snackTextView.SetTextSize(ComplexUnitType.Dip, (float)action.Font.FontSize); + } + snackActionButtonView.SetTypeface(action.Font.ToTypeface(), TypefaceStyle.Normal); } @@ -91,4 +121,4 @@ public override void OnDismissed(Java.Lang.Object transientBottomBar, int e) } } } -} \ No newline at end of file +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.gtk.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.gtk.cs index 666335678..62f50c825 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.gtk.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.gtk.cs @@ -1,4 +1,5 @@ -using System.Timers; +using System.Linq; +using System.Timers; using Gtk; using Pango; using Xamarin.CommunityToolkit.UI.Views.Options; @@ -10,24 +11,27 @@ namespace Xamarin.CommunityToolkit.UI.Views { class SnackBar { - Timer snackBarTimer; + Timer? snackBarTimer; public void Show(Page page, SnackBarOptions arguments) { var mainWindow = (Platform.GetRenderer(page).Container.Child as Forms.Platform.GTK.Controls.Page)?.Children[0] as VBox; var snackBarLayout = GetSnackBarLayout(mainWindow, arguments); + AddSnackBarContainer(mainWindow, snackBarLayout); + snackBarTimer = new Timer(arguments.Duration.TotalMilliseconds); snackBarTimer.Elapsed += (sender, e) => { - mainWindow.Remove(snackBarLayout); + mainWindow?.Remove(snackBarLayout); snackBarTimer.Stop(); arguments.SetResult(false); }; + snackBarTimer.Start(); } - HBox GetSnackBarLayout(Container container, SnackBarOptions arguments) + HBox GetSnackBarLayout(Container? container, SnackBarOptions arguments) { var snackBarLayout = new HBox(); snackBarLayout.ModifyBg(StateType.Normal, arguments.BackgroundColor.ToGtkColor()); @@ -35,6 +39,7 @@ HBox GetSnackBarLayout(Container container, SnackBarOptions arguments) var message = new Gtk.Label(arguments.MessageOptions.Message); message.ModifyFont(new FontDescription { AbsoluteSize = arguments.MessageOptions.Font.FontSize, Family = arguments.MessageOptions.Font.FontFamily }); message.ModifyFg(StateType.Normal, arguments.MessageOptions.Foreground.ToGtkColor()); + message.SetPadding((int)arguments.MessageOptions.Padding.Left, (int)arguments.MessageOptions.Padding.Top); snackBarLayout.Add(message); snackBarLayout.SetChildPacking(message, false, false, 0, PackType.Start); @@ -50,10 +55,13 @@ HBox GetSnackBarLayout(Container container, SnackBarOptions arguments) button.Clicked += async (sender, e) => { - snackBarTimer.Stop(); - await action.Action(); + snackBarTimer?.Stop(); + + if (action.Action != null) + await action.Action(); + arguments.SetResult(true); - container.Remove(snackBarLayout); + container?.Remove(snackBarLayout); }; snackBarLayout.Add(button); @@ -63,20 +71,20 @@ HBox GetSnackBarLayout(Container container, SnackBarOptions arguments) return snackBarLayout; } - void AddSnackBarContainer(Container mainWindow, Widget snackBarLayout) + void AddSnackBarContainer(Container? mainWindow, Widget snackBarLayout) { - var children = mainWindow.Children; - foreach (var child in mainWindow.Children) + var children = mainWindow?.Children ?? Enumerable.Empty(); + foreach (var child in children) { - mainWindow.Remove(child); + mainWindow?.Remove(child); } foreach (var child in children) { - mainWindow.Add(child); + mainWindow?.Add(child); } - mainWindow.Add(snackBarLayout); + mainWindow?.Add(snackBarLayout); snackBarLayout.ShowAll(); } } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.ios.macos.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.ios.macos.cs index a21b1fb96..42db8d690 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.ios.macos.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.ios.macos.cs @@ -43,6 +43,14 @@ internal void Show(Page sender, SnackBarOptions arguments) snackBar.Appearance.Foreground = arguments.MessageOptions.Foreground.ToUIColor(); } + if (arguments.MessageOptions.Padding != MessageOptions.DefaultPadding) + { + snackBar.Layout.PaddingTop = (nfloat)arguments.MessageOptions.Padding.Top; + snackBar.Layout.PaddingLeft = (nfloat)arguments.MessageOptions.Padding.Left; + snackBar.Layout.PaddingBottom = (nfloat)arguments.MessageOptions.Padding.Bottom; + snackBar.Layout.PaddingRight = (nfloat)arguments.MessageOptions.Padding.Right; + } + snackBar.Appearance.TextAlignment = arguments.IsRtl ? UITextAlignment.Right : UITextAlignment.Left; if (!UIDevice.CurrentDevice.CheckSystemVersion(11, 0)) @@ -71,43 +79,44 @@ internal void Show(Page sender, SnackBarOptions arguments) foreach (var action in arguments.Actions) { - var actionButton = new NativeActionButton(); + var actionButton = new NativeSnackButton(action.Padding.Left, + action.Padding.Top, + action.Padding.Right, + action.Padding.Bottom); actionButton.SetActionButtonText(action.Text); #if __IOS__ if (action.BackgroundColor != Color.Default) { - actionButton.Appearance.Background = action.BackgroundColor.ToUIColor(); + actionButton.BackgroundColor = action.BackgroundColor.ToUIColor(); } if (action.Font != Font.Default) { - actionButton.Appearance.Font = action.Font.ToUIFont(); + actionButton.Font = action.Font.ToUIFont(); } if (action.ForegroundColor != Color.Default) { - actionButton.Appearance.Foreground = action.ForegroundColor.ToUIColor(); + actionButton.SetTitleColor(action.ForegroundColor.ToUIColor(), UIControlState.Normal); } #elif __MACOS__ - if (action.BackgroundColor != Color.Default) + if (action.BackgroundColor != Color.Default && actionButton.Layer != null) { - actionButton.Appearance.Background = action.BackgroundColor.ToNSColor(); + actionButton.Layer.BackgroundColor = action.BackgroundColor.ToCGColor(); } if (action.Font != Font.Default) { - actionButton.Appearance.Font = action.Font.ToNSFont(); - } - - if (action.ForegroundColor != Color.Default) - { - actionButton.Appearance.Foreground = action.ForegroundColor.ToNSColor(); + actionButton.Font = action.Font.ToNSFont(); } #endif actionButton.SetAction(async () => { snackBar.Dismiss(); - await action.Action(); + + if (action.Action != null) + await action.Action(); + arguments.SetResult(true); }); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.netstandard.tvos.watchos.cs similarity index 78% rename from src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.shared.cs rename to src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.netstandard.tvos.watchos.cs index ea02eba70..fa30a2269 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.netstandard.tvos.watchos.cs @@ -1,5 +1,4 @@ -#if NETSTANDARD || __TVOS__ || __WATCHOS__ -using System; +using System; using Xamarin.CommunityToolkit.UI.Views.Options; using Xamarin.Forms; @@ -9,5 +8,4 @@ class SnackBar { internal void Show(Page sender, SnackBarOptions arguments) => throw new PlatformNotSupportedException(); } -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.tizen.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.tizen.cs index 8c1161cd6..0bcc49d81 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.tizen.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.tizen.cs @@ -27,14 +27,17 @@ internal void Show(Forms.Page sender, SnackBarOptions arguments) ok.Clicked += async (s, evt) => { snackBarDialog.Dismiss(); - await action.Action(); + + if (action.Action != null) + await action.Action(); + arguments.SetResult(true); }; } - snackBarDialog.TimedOut += (s, evt) => { DismissSnackBar(); }; + snackBarDialog.TimedOut += (s, evt) => DismissSnackBar(); - snackBarDialog.BackButtonPressed += (s, evt) => { DismissSnackBar(); }; + snackBarDialog.BackButtonPressed += (s, evt) => DismissSnackBar(); snackBarDialog.Show(); @@ -45,4 +48,4 @@ void DismissSnackBar() } } } -} \ No newline at end of file +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.uwp.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.uwp.cs index 83515a45e..b1218de27 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.uwp.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.uwp.cs @@ -10,9 +10,9 @@ namespace Xamarin.CommunityToolkit.UI.Views { class SnackBar { - DispatcherTimer snackBarTimer; + DispatcherTimer? snackBarTimer; - T FindVisualChildByName(DependencyObject parent, string name) where T : DependencyObject + T? FindVisualChildByName(DependencyObject parent, string name) where T : DependencyObject { var childrenCount = VisualTreeHelper.GetChildrenCount(parent); @@ -38,7 +38,7 @@ internal void Show(Forms.Page page, SnackBarOptions arguments) { var snackBarLayout = new SnackBarLayout(arguments); var pageControl = Platform.GetRenderer(page).ContainerElement.Parent; - var grid = FindVisualChildByName(pageControl, "BottomCommandBarArea").Parent as Grid; + var grid = (Grid)(FindVisualChildByName(pageControl, "BottomCommandBarArea")?.Parent ?? throw new NullReferenceException()); var snackBarRow = new RowDefinition() { Height = GridLength.Auto }; snackBarTimer = new DispatcherTimer { Interval = arguments.Duration }; snackBarTimer.Tick += (sender, e) => diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.wpf.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.wpf.cs index 464dc6659..9a40be781 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.wpf.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/Snackbar/SnackBar.wpf.cs @@ -9,7 +9,7 @@ namespace Xamarin.CommunityToolkit.UI.Views { class SnackBar { - Timer snackBarTimer; + Timer? snackBarTimer; internal void Show(Page page, SnackBarOptions arguments) { diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/StateLayout/StateLayout.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/StateLayout/StateLayout.shared.cs index f3fd13c44..5ce1b6be5 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/StateLayout/StateLayout.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/StateLayout/StateLayout.shared.cs @@ -19,10 +19,10 @@ public static class StateLayout internal static readonly BindableProperty LayoutControllerProperty = BindableProperty.CreateAttached("LayoutController", typeof(StateLayoutController), typeof(Layout), default(StateLayoutController), - defaultValueCreator: (b) => new StateLayoutController((Layout)b) { StateViews = GetStateViews(b) }); + defaultValueCreator: (b) => new StateLayoutController((Layout)b) { StateViews = GetStateViews(b) ?? new List() }); - internal static StateLayoutController GetLayoutController(BindableObject b) - => (StateLayoutController)b.GetValue(LayoutControllerProperty); + internal static StateLayoutController? GetLayoutController(BindableObject b) + => (StateLayoutController?)b.GetValue(LayoutControllerProperty); public static readonly BindableProperty CurrentStateProperty = BindableProperty.CreateAttached("CurrentState", typeof(LayoutState), typeof(Layout), default(LayoutState), @@ -36,8 +36,8 @@ internal static StateLayoutController GetLayoutController(BindableObject b) = BindableProperty.CreateAttached("AnimateStateChanges", typeof(bool), typeof(Layout), true, propertyChanged: (b, o, n) => ((BindableObject)b).SetValue(AnimateStateChangesProperty, n)); - public static IList GetStateViews(BindableObject b) - => (IList)b.GetValue(StateViewsProperty); + public static IList? GetStateViews(BindableObject b) + => (IList?)b.GetValue(StateViewsProperty); public static void SetCurrentState(BindableObject b, LayoutState value) => b.SetValue(CurrentStateProperty, value); @@ -48,8 +48,8 @@ public static LayoutState GetCurrentState(BindableObject b) public static void SetCurrentCustomStateKey(BindableObject b, string value) => b.SetValue(CurrentCustomStateKeyProperty, value); - public static string GetCurrentCustomStateKey(BindableObject b) - => (string)b.GetValue(CurrentCustomStateKeyProperty); + public static string? GetCurrentCustomStateKey(BindableObject b) + => (string?)b.GetValue(CurrentCustomStateKeyProperty); public static void SetAnimateStateChanges(BindableObject b, bool value) => b.SetValue(AnimateStateChangesProperty, value); @@ -68,10 +68,10 @@ static void OnCurrentStateChanged(BindableObject bindable, LayoutState oldValue, case LayoutState.Custom: break; case LayoutState.None: - GetLayoutController(bindable).SwitchToContent(GetAnimateStateChanges(bindable)); + GetLayoutController(bindable)?.SwitchToContent(GetAnimateStateChanges(bindable)); break; default: - GetLayoutController(bindable).SwitchToTemplate(newValue, null, GetAnimateStateChanges(bindable)); + GetLayoutController(bindable)?.SwitchToTemplate(newValue, null, GetAnimateStateChanges(bindable)); break; } } @@ -87,10 +87,10 @@ static void OnCurrentCustomStateKeyChanged(BindableObject bindable, string oldVa switch (state) { case LayoutState.None: - GetLayoutController(bindable).SwitchToContent(GetAnimateStateChanges(bindable)); + GetLayoutController(bindable)?.SwitchToContent(GetAnimateStateChanges(bindable)); break; case LayoutState.Custom: - GetLayoutController(bindable).SwitchToTemplate(newValue, GetAnimateStateChanges(bindable)); + GetLayoutController(bindable)?.SwitchToTemplate(newValue, GetAnimateStateChanges(bindable)); break; default: break; diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/StateLayout/StateLayoutController.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/StateLayout/StateLayoutController.shared.cs index be0d2c52d..7d4fad352 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/StateLayout/StateLayoutController.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/StateLayout/StateLayoutController.shared.cs @@ -11,11 +11,11 @@ public class StateLayoutController { readonly WeakReference> layoutWeakReference; bool layoutIsGrid; - LayoutState previousState; - IList originalContent; - CancellationTokenSource animationTokenSource; + LayoutState? previousState; + IList originalContent = Enumerable.Empty().ToList(); + CancellationTokenSource? animationTokenSource; - public IList StateViews { get; set; } + public IList StateViews { get; set; } = Enumerable.Empty().ToList(); public StateLayoutController(Layout layout) => layoutWeakReference = new WeakReference>(layout); @@ -48,7 +48,7 @@ public async void SwitchToContent(bool animate) public void SwitchToTemplate(string customState, bool animate) => SwitchToTemplate(LayoutState.Custom, customState, animate); - public async void SwitchToTemplate(LayoutState state, string customState, bool animate) + public async void SwitchToTemplate(LayoutState state, string? customState, bool animate) { if (!layoutWeakReference.TryGetTarget(out var layout)) return; @@ -161,7 +161,7 @@ public async void SwitchToTemplate(LayoutState state, string customState, bool a } } - StateView GetViewForState(LayoutState state, string customState) + StateView GetViewForState(LayoutState state, string? customState) { var view = StateViews.FirstOrDefault(x => (x.StateKey == state && state != LayoutState.Custom) || (state == LayoutState.Custom && x.CustomStateKey == customState)); @@ -169,7 +169,7 @@ StateView GetViewForState(LayoutState state, string customState) return view; } - int GetRepeatCount(LayoutState state, string customState) + int GetRepeatCount(LayoutState state, string? customState) { var template = StateViews.FirstOrDefault(x => (x.StateKey == state && state != LayoutState.Custom) || (state == LayoutState.Custom && x.CustomStateKey == customState)); @@ -180,7 +180,7 @@ int GetRepeatCount(LayoutState state, string customState) return 1; } - DataTemplate GetTemplate(LayoutState state, string customState) + DataTemplate? GetTemplate(LayoutState state, string? customState) { var view = StateViews.FirstOrDefault(x => (x.StateKey == state && state != LayoutState.Custom) || (state == LayoutState.Custom && x.CustomStateKey == customState)); @@ -191,7 +191,7 @@ DataTemplate GetTemplate(LayoutState state, string customState) return null; } - View CreateItemView(LayoutState state, string customState) + View CreateItemView(LayoutState state, string? customState) { var view = StateViews.FirstOrDefault(x => (x.StateKey == state && state != LayoutState.Custom) || (state == LayoutState.Custom && x.CustomStateKey == customState)); @@ -201,7 +201,7 @@ View CreateItemView(LayoutState state, string customState) if (view != null) return view; - return new Label() { Text = $"View for {state}{customState} not defined." }; + return new Label { Text = $"View for {state}{customState} not defined." }; } async Task ChildrenFadeTo(Layout layout, bool animate, bool isHide) diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/StateLayout/StateView.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/StateLayout/StateView.shared.cs index ba27cb819..4d0077364 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/StateLayout/StateView.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/StateLayout/StateView.shared.cs @@ -23,9 +23,9 @@ public LayoutState StateKey set => SetValue(StateKeyProperty, value); } - public string CustomStateKey + public string? CustomStateKey { - get => (string)GetValue(CustomStateKeyProperty); + get => (string?)GetValue(CustomStateKeyProperty); set => SetValue(CustomStateKeyProperty, value); } @@ -35,9 +35,9 @@ public int RepeatCount set => SetValue(RepeatCountProperty, value); } - public DataTemplate Template + public DataTemplate? Template { - get => (DataTemplate)GetValue(TemplateProperty); + get => (DataTemplate?)GetValue(TemplateProperty); set => SetValue(TemplateProperty, value); } } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/TabView/TabBadgeView.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/TabView/TabBadgeView.shared.cs index b1097b000..72b7b3757 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/TabView/TabBadgeView.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/TabView/TabBadgeView.shared.cs @@ -13,8 +13,8 @@ public class TabBadgeView : TemplatedView internal const string ElementBorder = "PART_Border"; internal const string ElementText = "PART_Text"; - Frame badgeBorder; - Label badgeText; + Frame? badgeBorder; + Label? badgeText; bool isVisible; public TabBadgeView() => ControlTemplate = new ControlTemplate(typeof(TabBadgeTemplate)); @@ -22,9 +22,9 @@ public class TabBadgeView : TemplatedView public static BindableProperty PlacementTargetProperty = BindableProperty.Create(nameof(PlacementTarget), typeof(View), typeof(TabBadgeView), null); - public View PlacementTarget + public View? PlacementTarget { - get => (View)GetValue(PlacementTargetProperty); + get => (View?)GetValue(PlacementTargetProperty); set => SetValue(PlacementTargetProperty, value); } @@ -38,7 +38,11 @@ public bool AutoHide set => SetValue(AutoHideProperty, value); } - static async void OnAutoHideChanged(BindableObject bindable, object oldValue, object newValue) => await (bindable as TabBadgeView)?.UpdateVisibilityAsync(); + static async void OnAutoHideChanged(BindableObject bindable, object oldValue, object newValue) + { + var tabBadgeView = (TabBadgeView)bindable; + await tabBadgeView.UpdateVisibilityAsync(); + } public static BindableProperty IsAnimatedProperty = BindableProperty.Create(nameof(IsAnimated), typeof(bool), typeof(TabBadgeView), defaultValue: true); @@ -52,9 +56,9 @@ public bool IsAnimated public static BindableProperty BadgeAnimationProperty = BindableProperty.Create(nameof(BadgeAnimation), typeof(IBadgeAnimation), typeof(TabBadgeView), new BadgeAnimation()); - public IBadgeAnimation BadgeAnimation + public IBadgeAnimation? BadgeAnimation { - get => (IBadgeAnimation)GetValue(BadgeAnimationProperty); + get => (IBadgeAnimation?)GetValue(BadgeAnimationProperty); set => SetValue(BadgeAnimationProperty, value); } @@ -110,23 +114,23 @@ protected override void OnApplyTemplate() { base.OnApplyTemplate(); - badgeBorder = GetTemplateChild(ElementBorder) as Frame; - badgeText = GetTemplateChild(ElementText) as Label; + badgeBorder = (Frame)GetTemplateChild(ElementBorder); + badgeText = (Label)GetTemplateChild(ElementText); UpdateSize(); - UpdatePosition(); - UpdateIsEnabled(); + UpdatePosition(badgeBorder); + UpdateIsEnabled(badgeText); } - protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) + protected override void OnPropertyChanged([CallerMemberName] string propertyName = "") { base.OnPropertyChanged(propertyName); - if (propertyName == IsEnabledProperty.PropertyName) - UpdateIsEnabled(); + if (propertyName == IsEnabledProperty.PropertyName && badgeText is Label label) + UpdateIsEnabled(label); } - void UpdateIsEnabled() + void UpdateIsEnabled(in Label badgeText) { if (IsEnabled) badgeText.PropertyChanged += OnBadgeTextPropertyChanged; @@ -134,21 +138,18 @@ void UpdateIsEnabled() badgeText.PropertyChanged -= OnBadgeTextPropertyChanged; } - void OnBadgeTextPropertyChanged(object sender, PropertyChangedEventArgs e) + void OnBadgeTextPropertyChanged(object? sender, PropertyChangedEventArgs e) { - switch (e.PropertyName) + if (e.PropertyName is nameof(Height) or nameof(Width) && badgeBorder is Frame frame) { - case nameof(Height): - case nameof(Width): - UpdateSize(); - UpdatePosition(); - break; + UpdateSize(); + UpdatePosition(frame); } } void UpdateSize() { - if (badgeText == null || badgeText.Width <= 0 || badgeText.Height <= 0) + if (badgeBorder == null || badgeText == null || badgeText.Width <= 0 || badgeText.Height <= 0) return; var badgeTextHeight = badgeText.Height + (badgeBorder.Padding.VerticalThickness / 2); @@ -160,7 +161,7 @@ void UpdateSize() badgeBorder.CornerRadius = (int)Math.Round(badgeTextHeight / 2); } - void UpdatePosition() + void UpdatePosition(Frame badgeBorder) { if (PlacementTarget == null) return; @@ -173,23 +174,38 @@ void UpdatePosition() badgeBorder.Margin = new Thickness(x, 0, 0, 0); } - void UpdateBackgroundColor(Color backgroundColor) => badgeBorder.BackgroundColor = backgroundColor; + void UpdateBackgroundColor(Color backgroundColor) + { + if (badgeBorder != null) + badgeBorder.BackgroundColor = backgroundColor; + } - void UpdateBorderColor(Color borderColor) => badgeBorder.BorderColor = borderColor; + void UpdateBorderColor(Color borderColor) + { + if (badgeBorder != null) + badgeBorder.BorderColor = borderColor; + } - void UpdateTextColor(Color textColor) => badgeText.TextColor = textColor; + void UpdateTextColor(Color textColor) + { + if (badgeText != null) + badgeText.TextColor = textColor; + } async void UpdateText(string text) { - badgeText.Text = text; - await UpdateVisibilityAsync(); + if (badgeText != null) + { + badgeText.Text = text; + await UpdateVisibilityAsync(); + } } async Task UpdateVisibilityAsync() { - var badgeText = this.badgeText.Text; + var badgeText = this.badgeText?.Text; - if (string.IsNullOrEmpty(badgeText)) + if (badgeText == null || string.IsNullOrEmpty(badgeText)) { IsVisible = false; return; @@ -205,11 +221,15 @@ async Task UpdateVisibilityAsync() if (badgeIsVisible) { IsVisible = true; - await BadgeAnimation.OnAppearing(this); + + if (BadgeAnimation != null) + await BadgeAnimation.OnAppearing(this); } else { - await BadgeAnimation.OnDisappering(this); + if (BadgeAnimation != null) + await BadgeAnimation.OnDisappering(this); + IsVisible = false; } diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/TabView/TabView.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/TabView/TabView.shared.cs index c063723e8..4553fae10 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/TabView/TabView.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/TabView/TabView.shared.cs @@ -29,8 +29,8 @@ public class TabView : ContentView, IDisposable readonly CarouselView contentContainer; readonly List contentWidthCollection; - IList tabItemsSource; - ObservableCollection contentTabItems; + IList? tabItemsSource; + ObservableCollection? contentTabItems; public TabView() { @@ -172,6 +172,9 @@ public void Dispose() if (TabItems != null) TabItems.CollectionChanged -= OnTabItemsCollectionChanged; + + var lazyView = ((contentContainer?.CurrentItem as TabViewItem)?.Content as BaseLazyView) ?? (TabItems?[SelectedIndex].Content as BaseLazyView); + lazyView?.Dispose(); } public ObservableCollection TabItems { get; set; } @@ -180,9 +183,9 @@ public void Dispose() BindableProperty.Create(nameof(TabItemsSource), typeof(IList), typeof(TabView), null, propertyChanged: OnTabItemsSourceChanged); - public IList TabItemsSource + public IList? TabItemsSource { - get => (IList)GetValue(TabItemsSourceProperty); + get => (IList?)GetValue(TabItemsSourceProperty); set => SetValue(TabItemsSourceProperty, value); } @@ -191,18 +194,18 @@ public IList TabItemsSource public static readonly BindableProperty TabViewItemDataTemplateProperty = BindableProperty.Create(nameof(TabViewItemDataTemplate), typeof(DataTemplate), typeof(TabView), null); - public DataTemplate TabViewItemDataTemplate + public DataTemplate? TabViewItemDataTemplate { - get => (DataTemplate)GetValue(TabViewItemDataTemplateProperty); + get => (DataTemplate?)GetValue(TabViewItemDataTemplateProperty); set => SetValue(TabViewItemDataTemplateProperty, value); } public static readonly BindableProperty TabContentDataTemplateProperty = BindableProperty.Create(nameof(TabContentDataTemplate), typeof(DataTemplate), typeof(TabView), null); - public DataTemplate TabContentDataTemplate + public DataTemplate? TabContentDataTemplate { - get => (DataTemplate)GetValue(TabContentDataTemplateProperty); + get => (DataTemplate?)GetValue(TabContentDataTemplateProperty); set => SetValue(TabContentDataTemplateProperty, value); } @@ -259,9 +262,9 @@ public Color TabStripBackgroundColor BindableProperty.Create(nameof(TabStripBackgroundColor), typeof(View), typeof(TabView), null, propertyChanged: OnTabStripBackgroundViewChanged); - public View TabStripBackgroundView + public View? TabStripBackgroundView { - get => (View)GetValue(TabStripBackgroundViewProperty); + get => (View?)GetValue(TabStripBackgroundViewProperty); set => SetValue(TabStripBackgroundViewProperty, value); } @@ -367,9 +370,9 @@ public double TabIndicatorWidth BindableProperty.Create(nameof(TabIndicatorView), typeof(View), typeof(TabView), null, propertyChanged: OnTabIndicatorViewChanged); - public View TabIndicatorView + public View? TabIndicatorView { - get => (View)GetValue(TabIndicatorViewProperty); + get => (View?)GetValue(TabIndicatorViewProperty); set => SetValue(TabIndicatorViewProperty, value); } @@ -411,15 +414,15 @@ public bool IsSwipeEnabled static void OnIsSwipeEnabledChanged(BindableObject bindable, object oldValue, object newValue) => (bindable as TabView)?.UpdateIsSwipeEnabled((bool)newValue); - public delegate void TabSelectionChangedEventHandler(object sender, TabSelectionChangedEventArgs e); + public delegate void TabSelectionChangedEventHandler(object? sender, TabSelectionChangedEventArgs e); - public event TabSelectionChangedEventHandler SelectionChanged; + public event TabSelectionChangedEventHandler? SelectionChanged; - public delegate void TabViewScrolledEventHandler(object sender, ItemsViewScrolledEventArgs e); + public delegate void TabViewScrolledEventHandler(object? sender, ItemsViewScrolledEventArgs e); - public event TabViewScrolledEventHandler Scrolled; + public event TabViewScrolledEventHandler? Scrolled; - protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) + protected override void OnPropertyChanged([CallerMemberName] string propertyName = "") { base.OnPropertyChanged(propertyName); @@ -438,37 +441,38 @@ protected override void OnBindingContextChanged() UpdateTabViewItemBindingContext(tabViewItem); } - void OnTabViewItemPropertyChanged(object sender, PropertyChangedEventArgs e) + void OnTabViewItemPropertyChanged(object? sender, PropertyChangedEventArgs e) { - var tabViewItem = (TabViewItem)sender; - - if (e.PropertyName == TabViewItem.TabWidthProperty.PropertyName) - UpdateTabViewItemTabWidth(tabViewItem); + if (sender is TabViewItem tabViewItem) + { + if (e.PropertyName == TabViewItem.TabWidthProperty.PropertyName) + UpdateTabViewItemTabWidth(tabViewItem); + } } - void OnTabItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + void OnTabItemsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (e.OldItems != null) { - foreach (TabViewItem oldItem in e.OldItems) + foreach (var tabViewItem in e.OldItems.OfType()) { - ClearTabViewItem(oldItem); + ClearTabViewItem(tabViewItem); } } if (e.NewItems != null) { - foreach (TabViewItem newTabViewItem in e.NewItems) + foreach (var tabViewItem in e.NewItems.OfType()) { - AddTabViewItem(newTabViewItem, TabItems.IndexOf(newTabViewItem)); + AddTabViewItem(tabViewItem, TabItems.IndexOf(tabViewItem)); } } } - void OnContentContainerPropertyChanged(object sender, PropertyChangedEventArgs e) + void OnContentContainerPropertyChanged(object? sender, PropertyChangedEventArgs e) { - if (e.PropertyName == nameof(CarouselView.ItemsSource) || - e.PropertyName == nameof(CarouselView.VisibleViews)) + if (e.PropertyName == nameof(CarouselView.ItemsSource) + || e.PropertyName == nameof(CarouselView.VisibleViews)) { var items = contentContainer.ItemsSource; @@ -482,7 +486,7 @@ void OnContentContainerPropertyChanged(object sender, PropertyChangedEventArgs e } } - void OnContentContainerScrolled(object sender, ItemsViewScrolledEventArgs args) + void OnContentContainerScrolled(object? sender, ItemsViewScrolledEventArgs args) { for (var i = 0; i < TabItems.Count; i++) TabItems[i].UpdateCurrentContent(); @@ -502,7 +506,7 @@ void ClearTabStrip() tabStripContent.ColumnDefinitions.Clear(); - var hasItems = TabItems.Count > 0 || TabItemsSource.Count > 0; + var hasItems = TabItems.Count > 0 || TabItemsSource?.Count > 0; tabStripContainer.IsVisible = hasItems; } @@ -567,7 +571,7 @@ void UpdateTabContentSize() UpdateTabContentHeight(count != 0 ? TabContentHeight : 0); } - void AddTabViewItemFromTemplate(object item, int index = -1) => AddTabViewItemFromTemplateToTabStrip(item, index); + void AddTabViewItemFromTemplate(object? item, int index = -1) => AddTabViewItemFromTemplateToTabStrip(item, index); void UpdateTabViewItemBindingContext(TabViewItem tabViewItem) { @@ -581,9 +585,12 @@ void AddSelectionTapRecognizer(View view) { var tapRecognizer = new TapGestureRecognizer(); - tapRecognizer.Tapped += (object sender, EventArgs args) => + tapRecognizer.Tapped += (object? sender, EventArgs args) => { - var capturedIndex = tabStripContent.Children.IndexOf((View)sender); + if (sender is not View view) + return; + + var capturedIndex = tabStripContent.Children.IndexOf(view); if (view is TabViewItem tabViewItem) { @@ -633,11 +640,11 @@ void AddTabViewItemToTabStrip(View item, int index = -1) UpdateTabViewItemTabWidth(item as TabViewItem); } - void AddTabViewItemFromTemplateToTabStrip(object item, int index = -1) + void AddTabViewItemFromTemplateToTabStrip(object? item, int index = -1) { - var view = !(TabViewItemDataTemplate is DataTemplateSelector tabItemDataTemplate) ? - (View)TabViewItemDataTemplate.CreateContent() : - (View)tabItemDataTemplate.SelectTemplate(item, this).CreateContent(); + var view = TabViewItemDataTemplate is not DataTemplateSelector tabItemDataTemplate + ? (View)(TabViewItemDataTemplate?.CreateContent() ?? throw new NullReferenceException()) + : (View)tabItemDataTemplate.SelectTemplate(item, this).CreateContent(); view.BindingContext = item; @@ -668,7 +675,7 @@ void UpdateIsEnabled() contentContainer.IsEnabled = IsEnabled; } - void UpdateTabViewItemTabWidth(TabViewItem tabViewItem) + void UpdateTabViewItemTabWidth(TabViewItem? tabViewItem) { if (tabViewItem == null) return; @@ -676,7 +683,7 @@ void UpdateTabViewItemTabWidth(TabViewItem tabViewItem) var index = tabStripContent.Children.IndexOf(tabViewItem); var colummns = tabStripContent.ColumnDefinitions; - ColumnDefinition column = null; + ColumnDefinition? column = null; if (index < colummns.Count) column = colummns[index]; @@ -707,7 +714,9 @@ void UpdateTabItemsSource() contentContainer.ItemsSource = TabItemsSource; foreach (var item in TabItemsSource) + { AddTabViewItemFromTemplate(item); + } UpdateTabContentSize(); UpdateTabStripSize(); @@ -715,7 +724,7 @@ void UpdateTabItemsSource() UpdateSelectedIndex(0); } - void OnTabItemsSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => UpdateTabItemsSource(); + void OnTabItemsSourceCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) => UpdateTabItemsSource(); void UpdateItemsSource(IEnumerable items) { @@ -767,13 +776,15 @@ void UpdateSelectedIndex(int position, bool hasCurrentItem = false) if (TabItems.Count > 0) { - TabViewItem currentItem = null; + TabViewItem? currentItem = null; if (hasCurrentItem) currentItem = (TabViewItem)contentContainer.CurrentItem; var tabViewItem = TabItems[position]; + var lazyView = (currentItem?.Content as BaseLazyView) ?? (tabViewItem.Content as BaseLazyView); + contentIndex = contentTabItems.IndexOf(currentItem ?? tabViewItem); tabStripIndex = TabItems.IndexOf(currentItem ?? tabViewItem); @@ -787,6 +798,9 @@ void UpdateSelectedIndex(int position, bool hasCurrentItem = false) TabItems[index].IsSelected = false; } + if (lazyView != null && !lazyView.IsLoaded) + await lazyView.LoadViewAsync(); + var currentTabItem = TabItems[position]; currentTabItem.SizeChanged += OnCurrentTabItemSizeChanged; UpdateTabIndicatorPosition(currentTabItem); @@ -814,9 +828,12 @@ void UpdateSelectedIndex(int position, bool hasCurrentItem = false) }); } - void OnCurrentTabItemSizeChanged(object sender, EventArgs e) + void OnCurrentTabItemSizeChanged(object? sender, EventArgs e) { - var currentTabItem = (View)sender; + if (sender is not View view) + return; + + var currentTabItem = view; UpdateTabIndicatorWidth(TabIndicatorWidth > 0 ? TabIndicatorWidth : currentTabItem.Width); UpdateTabIndicatorPosition(currentTabItem); currentTabItem.SizeChanged -= OnCurrentTabItemSizeChanged; @@ -941,11 +958,27 @@ void UpdateIsTabStripVisible(bool isTabStripVisible) void UpdateTabContentHeight(double tabContentHeight) => contentContainer.HeightRequest = tabContentHeight; - void UpdateTabIndicatorColor(Color tabIndicatorColor) => tabStripIndicator.BackgroundColor = tabIndicatorColor; + void UpdateTabIndicatorColor(Color tabIndicatorColor) + { + if (tabStripIndicator != null) + tabStripIndicator.BackgroundColor = tabIndicatorColor; + } - void UpdateTabIndicatorHeight(double tabIndicatorHeight) => tabStripIndicator.HeightRequest = tabIndicatorHeight; + void UpdateTabIndicatorHeight(double tabIndicatorHeight) + { + if (tabStripIndicator != null) + { + tabStripIndicator.HeightRequest = tabIndicatorHeight; + } + } - void UpdateTabIndicatorWidth(double tabIndicatorWidth) => tabStripIndicator.WidthRequest = tabIndicatorWidth; + void UpdateTabIndicatorWidth(double tabIndicatorWidth) + { + if (tabStripIndicator != null) + { + tabStripIndicator.WidthRequest = tabIndicatorWidth; + } + } void UpdateTabIndicatorView(View tabIndicatorView) { @@ -974,9 +1007,21 @@ void UpdateTabIndicatorPlacement(TabIndicatorPlacement tabIndicatorPlacement) UpdateTabIndicatorMargin(); } - void UpdateIsSwipeEnabled(bool isSwipeEnabled) => contentContainer.IsSwipeEnabled = isSwipeEnabled; + void UpdateIsSwipeEnabled(bool isSwipeEnabled) + { + if (contentContainer != null) + { + contentContainer.IsSwipeEnabled = isSwipeEnabled; + } + } - void UpdateIsTabTransitionEnabled(bool isTabTransitionEnabled) => contentContainer.IsScrollAnimated = isTabTransitionEnabled; + void UpdateIsTabTransitionEnabled(bool isTabTransitionEnabled) + { + if (contentContainer != null) + { + contentContainer.IsScrollAnimated = isTabTransitionEnabled; + } + } void UpdateTabIndicatorPosition(int tabViewItemIndex) { diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/TabView/TabViewItem.shared.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/TabView/TabViewItem.shared.cs index 30061db71..9e9007937 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/TabView/TabViewItem.shared.cs +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/TabView/TabViewItem.shared.cs @@ -18,9 +18,9 @@ public class TabViewItem : TemplatedView public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(TabViewItem), string.Empty); - public string Text + public string? Text { - get => (string)GetValue(TextProperty); + get => (string?)GetValue(TextProperty); set => SetValue(TextProperty, value); } @@ -105,9 +105,9 @@ public FontAttributes FontAttributesSelected public static readonly BindableProperty ContentProperty = BindableProperty.Create(nameof(Content), typeof(View), typeof(TabViewItem)); - public View Content + public View? Content { - get => (View)GetValue(ContentProperty); + get => (View?)GetValue(ContentProperty); set => SetValue(ContentProperty, value); } @@ -115,9 +115,9 @@ public View Content BindableProperty.Create(nameof(Icon), typeof(ImageSource), typeof(TabViewItem), null, propertyChanged: OnTabViewItemPropertyChanged); - public ImageSource Icon + public ImageSource? Icon { - get => (ImageSource)GetValue(IconProperty); + get => (ImageSource?)GetValue(IconProperty); set => SetValue(IconProperty, value); } @@ -125,9 +125,9 @@ public ImageSource Icon BindableProperty.Create(nameof(IconSelected), typeof(ImageSource), typeof(TabViewItem), null, propertyChanged: OnTabViewItemPropertyChanged); - public ImageSource IconSelected + public ImageSource? IconSelected { - get => (ImageSource)GetValue(IconSelectedProperty); + get => (ImageSource?)GetValue(IconSelectedProperty); set => SetValue(IconSelectedProperty, value); } @@ -165,9 +165,9 @@ public double TabWidth public static BindableProperty TabAnimationProperty = BindableProperty.Create(nameof(TabAnimation), typeof(ITabViewItemAnimation), typeof(TabViewItem), null); - public ITabViewItemAnimation TabAnimation + public ITabViewItemAnimation? TabAnimation { - get => (ITabViewItemAnimation)GetValue(TabAnimationProperty); + get => (ITabViewItemAnimation?)GetValue(TabAnimationProperty); set => SetValue(TabAnimationProperty, value); } @@ -261,9 +261,9 @@ public double CurrentFontSize public static readonly BindableProperty CurrentIconProperty = CurrentIconPropertyKey.BindableProperty; - public ImageSource CurrentIcon + public ImageSource? CurrentIcon { - get => (ImageSource)GetValue(CurrentIconProperty); + get => (ImageSource?)GetValue(CurrentIconProperty); private set => SetValue(CurrentIconPropertyKey, value); } @@ -311,17 +311,17 @@ public Color CurrentBadgeBorderColor public static readonly BindableProperty CurrentContentProperty = CurrentContentPropertyKey.BindableProperty; - public View CurrentContent + public View? CurrentContent { - get => (View)GetValue(CurrentContentProperty); + get => (View?)GetValue(CurrentContentProperty); private set => SetValue(CurrentContentPropertyKey, value); } - public delegate void TabTappedEventHandler(object sender, TabTappedEventArgs e); + public delegate void TabTappedEventHandler(object? sender, TabTappedEventArgs e); - public event TabTappedEventHandler TabTapped; + public event TabTappedEventHandler? TabTapped; - protected override void OnPropertyChanged([CallerMemberName] string propertyName = null) + protected override void OnPropertyChanged([CallerMemberName] string propertyName = "") { base.OnPropertyChanged(propertyName); diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/ViewToRendererConverter.uwp.cs b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/ViewToRendererConverter.uwp.cs new file mode 100644 index 000000000..60c049a33 --- /dev/null +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Views/ViewToRendererConverter.uwp.cs @@ -0,0 +1,100 @@ +using System; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Xamarin.CommunityToolkit.Extensions; +using Xamarin.Forms; +using Xamarin.Forms.Platform.UWP; +using WRect = Windows.Foundation.Rect; + +namespace Xamarin.CommunityToolkit.UI.Views +{ + public class ViewToRendererConverter + { + // This class is ported from Xamarin.Forms and should remain in sync. + // This is used in the PopupRenderer.uwp.cs + internal class WrapperControl : Panel + { + readonly View view; + + FrameworkElement FrameworkElement { get; } + + public WrapperControl(View view) + { + this.view = view; + this.view.MeasureInvalidated += OnMeasureInvalidated; + + var renderer = Platform.CreateRenderer(view); + Platform.SetRenderer(view, renderer); + + FrameworkElement = renderer.ContainerElement; + Children.Add(renderer.ContainerElement); + + // make sure we re-measure once the template is applied + FrameworkElement.Loaded += (sender, args) => + { + // If the view is a layout (stacklayout, grid, etc) we need to trigger a layout pass + // with all the controls in a consistent native state (i.e., loaded) so they'll actually + // have Bounds set + (this.view as Layout)?.ForceLayout(); + InvalidateMeasure(); + }; + } + + internal void CleanUp() + { + view.Cleanup(); + view.MeasureInvalidated -= OnMeasureInvalidated; + } + + void OnMeasureInvalidated(object? sender, EventArgs e) + { + InvalidateMeasure(); + } + + protected override Windows.Foundation.Size ArrangeOverride(Windows.Foundation.Size finalSize) + { + view.IsInNativeLayout = true; + Layout.LayoutChildIntoBoundingRegion(view, new Rectangle(0, 0, finalSize.Width, finalSize.Height)); + + if (view.Width <= 0 || view.Height <= 0) + { + // Hide Panel when size _view is empty. + // It is necessary that this element does not overlap other elements when it should be hidden. + Opacity = 0; + } + else + { + Opacity = 1; + FrameworkElement?.Arrange(new WRect(view.X, view.Y, view.Width, view.Height)); + } + view.IsInNativeLayout = false; + + return finalSize; + } + + protected override Windows.Foundation.Size MeasureOverride(Windows.Foundation.Size availableSize) + { + var request = view.Measure(availableSize.Width, availableSize.Height, MeasureFlags.IncludeMargins).Request; + + if (request.Height < 0) + { + request.Height = availableSize.Height; + } + + Windows.Foundation.Size result; + if (view.HorizontalOptions.Alignment == LayoutAlignment.Fill && !double.IsInfinity(availableSize.Width) && availableSize.Width != 0) + { + result = new Windows.Foundation.Size(availableSize.Width, request.Height); + } + else + { + result = new Windows.Foundation.Size(request.Width, request.Height); + } + + FrameworkElement?.Measure(availableSize); + + return result; + } + } + } +} diff --git a/src/CommunityToolkit/Xamarin.CommunityToolkit/Xamarin.CommunityToolkit.csproj b/src/CommunityToolkit/Xamarin.CommunityToolkit/Xamarin.CommunityToolkit.csproj index 5fd5845e7..ac07310c1 100644 --- a/src/CommunityToolkit/Xamarin.CommunityToolkit/Xamarin.CommunityToolkit.csproj +++ b/src/CommunityToolkit/Xamarin.CommunityToolkit/Xamarin.CommunityToolkit.csproj @@ -2,7 +2,7 @@ netstandard1.0;netstandard2.0;netstandard2.1;Xamarin.iOS10;MonoAndroid10.0;Xamarin.TVOS10;Xamarin.WatchOS10;Xamarin.Mac20;tizen40 - $(TargetFrameworks);uap10.0.17763;netcoreapp3.1;net472;net471 + $(TargetFrameworks);uap10.0.19041;netcoreapp3.1;net472;net471 Xamarin.CommunityToolkit Xamarin.CommunityToolkit Xamarin.CommunityToolkit @@ -28,9 +28,9 @@ MIT true https://github.com/xamarin/XamarinCommunityToolkit - portable Debug;Release false + portable SA1123 @@ -40,15 +40,6 @@ - - - true - - true - - true - snupkg - true true @@ -56,13 +47,18 @@ UWP - - bin\Debug\netstandard1.0\Xamarin.CommunityToolkit.xml + + $(DefineConstants);UWP_14393 + + + $(DefineConstants);UWP_16299;UWP_14393 + + + $(DefineConstants);UWP_18362;UWP_16299;UWP_14393 + + + $(DefineConstants);UWP_19041;UWP_18362;UWP_16299;UWP_14393 - - - - @@ -70,6 +66,14 @@ + + + + + + + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 000000000..0c98cab25 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,26 @@ + + + + embedded + + true + + true + + true + snupkg + + + + + + + + full + true + + + + + + \ No newline at end of file diff --git a/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BaseTestFixture.cs b/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BaseTestFixture.cs index eb6feb334..8834e4e66 100644 --- a/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BaseTestFixture.cs +++ b/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BaseTestFixture.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System; +using System.Globalization; using NUnit.Framework; using Xamarin.Forms; @@ -6,8 +7,8 @@ namespace Xamarin.CommunityToolkit.Markup.UnitTests { public class BaseTestFixture { - CultureInfo defaultCulture; - CultureInfo defaultUICulture; + CultureInfo? defaultCulture; + CultureInfo? defaultUICulture; [SetUp] public virtual void Setup() @@ -21,8 +22,9 @@ public virtual void Setup() public virtual void TearDown() { Device.PlatformServices = null; - System.Threading.Thread.CurrentThread.CurrentCulture = defaultCulture; - System.Threading.Thread.CurrentThread.CurrentUICulture = defaultUICulture; + + System.Threading.Thread.CurrentThread.CurrentCulture = defaultCulture ?? throw new NullReferenceException(); + System.Threading.Thread.CurrentThread.CurrentUICulture = defaultUICulture ?? throw new NullReferenceException(); } } } \ No newline at end of file diff --git a/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BindableLayoutExtensionsTests.cs b/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BindableLayoutExtensionsTests.cs index 923cf13be..2e0472e88 100644 --- a/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BindableLayoutExtensionsTests.cs +++ b/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BindableLayoutExtensionsTests.cs @@ -11,21 +11,21 @@ public class BindableLayoutExtensionsTests : MarkupBaseTestFixture public void EmptyView() { var view = new BoxView(); - TestPropertiesSet(l => l.EmptyView(view), (BindableLayout.EmptyViewProperty, view)); + TestPropertiesSet(l => l?.EmptyView(view), (BindableLayout.EmptyViewProperty, view)); } [Test] public void EmptyViewTemplate() { var template = new DataTemplate(() => new BoxView()); - TestPropertiesSet(l => l.EmptyViewTemplate(template), (BindableLayout.EmptyViewTemplateProperty, template)); + TestPropertiesSet(l => l?.EmptyViewTemplate(template), (BindableLayout.EmptyViewTemplateProperty, template)); } [Test] public void EmptyViewTemplateFunction() { Func loadTemplate = () => new BoxView(); - Bindable.EmptyViewTemplate(loadTemplate); + Bindable?.EmptyViewTemplate(loadTemplate); Assert.That(BindableLayout.GetEmptyViewTemplate(Bindable), Is.Not.Null); } @@ -33,21 +33,21 @@ public void EmptyViewTemplateFunction() public void ItemsSource() { var source = new string[] { }; - TestPropertiesSet(l => l.ItemsSource(source), (BindableLayout.ItemsSourceProperty, source)); + TestPropertiesSet(l => l?.ItemsSource(source), (BindableLayout.ItemsSourceProperty, source)); } [Test] public void ItemTemplate() { var template = new DataTemplate(() => new BoxView()); - TestPropertiesSet(l => l.ItemTemplate(template), (BindableLayout.ItemTemplateProperty, template)); + TestPropertiesSet(l => l?.ItemTemplate(template), (BindableLayout.ItemTemplateProperty, template)); } [Test] public void ItemTemplateFunction() { Func loadTemplate = () => new BoxView(); - Bindable.ItemTemplate(loadTemplate); + Bindable?.ItemTemplate(loadTemplate); Assert.That(BindableLayout.GetItemTemplate(Bindable), Is.Not.Null); } @@ -55,7 +55,7 @@ public void ItemTemplateFunction() public void ItemTemplateSelector() { var selector = new Selector(); - TestPropertiesSet(l => l.ItemTemplateSelector(selector), (BindableLayout.ItemTemplateSelectorProperty, selector)); + TestPropertiesSet(l => l?.ItemTemplateSelector(selector), (BindableLayout.ItemTemplateSelectorProperty, selector)); } class Selector : DataTemplateSelector diff --git a/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BindableObjectExtensionsTests.cs b/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BindableObjectExtensionsTests.cs index 43df590e0..2f449f7ec 100644 --- a/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BindableObjectExtensionsTests.cs +++ b/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BindableObjectExtensionsTests.cs @@ -10,7 +10,7 @@ namespace Xamarin.CommunityToolkit.Markup.UnitTests [TestFixture] public class BindableObjectExtensionsTests : MarkupBaseTestFixture { - ViewModel viewModel; + ViewModel? viewModel; [SetUp] public override void Setup() @@ -41,7 +41,7 @@ public void BindSpecifiedPropertyWithPositionalParameters() { var button = new Button(); object converterParameter = 1; - string stringFormat = nameof(BindSpecifiedPropertyWithPositionalParameters) + " {0}"; + var stringFormat = nameof(BindSpecifiedPropertyWithPositionalParameters) + " {0}"; IValueConverter converter = new ToStringConverter(); object source = new ViewModel(); object targetNullValue = nameof(BindSpecifiedPropertyWithPositionalParameters) + " null"; @@ -80,7 +80,7 @@ public void BindSpecifiedPropertyWithInlineOneWayConvertAndDefaults() label.Bind( Label.TextColorProperty, nameof(viewModel.IsRed), - convert: (bool isRed) => isRed ? Color.Red : Color.Transparent + convert: (bool? isRed) => isRed.HasValue && isRed.Value ? Color.Red : Color.Transparent ); BindingHelpers.AssertBindingExists( @@ -88,7 +88,7 @@ public void BindSpecifiedPropertyWithInlineOneWayConvertAndDefaults() Label.TextColorProperty, nameof(viewModel.IsRed), assertConverterInstanceIsAnyNotNull: true, - assertConvert: c => c.AssertConvert(true, Color.Red).AssertConvert(false, Color.Transparent) + assertConvert: c => c.AssertConvert(true, Color.Red).AssertConvert(false, Color.Transparent) ); } @@ -99,7 +99,7 @@ public void BindSpecifiedPropertyWithInlineOneWayParameterizedConvertAndDefaults label.Bind( Label.TextColorProperty, nameof(viewModel.IsRed), - convert: (bool isRed, double alpha) => (isRed ? Color.Red : Color.Green).MultiplyAlpha(alpha), + convert: (bool? isRed, double? alpha) => (isRed.HasValue && isRed.Value ? Color.Red : Color.Green).MultiplyAlpha(alpha ?? throw new NullReferenceException()), converterParameter: 0.5 ); @@ -109,8 +109,8 @@ public void BindSpecifiedPropertyWithInlineOneWayParameterizedConvertAndDefaults nameof(viewModel.IsRed), assertConverterInstanceIsAnyNotNull: true, converterParameter: 0.5, - assertConvert: c => c.AssertConvert(true, 0.5, Color.Red.MultiplyAlpha(0.5)) - .AssertConvert(false, 0.2, Color.Green.MultiplyAlpha(0.2)) + assertConvert: c => c.AssertConvert(true, 0.5, Color.Red.MultiplyAlpha(0.5)) + .AssertConvert(false, 0.2, Color.Green.MultiplyAlpha(0.2)) ); } @@ -122,7 +122,7 @@ public void BindSpecifiedPropertyWithInlineTwoWayConvertAndDefaults() Label.TextColorProperty, nameof(viewModel.IsRed), BindingMode.TwoWay, - (bool isRed) => isRed ? Color.Red : Color.Transparent, + (bool? isRed) => isRed.HasValue && isRed.Value ? Color.Red : Color.Transparent, color => color == Color.Red ); @@ -145,8 +145,8 @@ public void BindSpecifiedPropertyWithInlineTwoWayParameterizedConvertAndDefaults Label.TextColorProperty, nameof(viewModel.IsRed), BindingMode.TwoWay, - (bool isRed, double alpha) => (isRed ? Color.Red : Color.Green).MultiplyAlpha(alpha), - (color, alpha) => color == Color.Red.MultiplyAlpha(alpha), + (bool? isRed, double? alpha) => (isRed.HasValue && isRed.Value ? Color.Red : Color.Green).MultiplyAlpha(alpha ?? throw new NullReferenceException()), + (color, alpha) => color == Color.Red.MultiplyAlpha(alpha ?? throw new NullReferenceException()), 0.5 ); @@ -166,16 +166,16 @@ public void BindSpecifiedPropertyWithInlineTwoWayParameterizedConvertAndDefaults public void BindSpecifiedPropertyWithInlineOneWayConvertAndPositionalParameters() { var button = new Button(); - string stringFormat = nameof(BindSpecifiedPropertyWithInlineOneWayConvertAndPositionalParameters) + " {0}"; + var stringFormat = nameof(BindSpecifiedPropertyWithInlineOneWayConvertAndPositionalParameters) + " {0}"; object source = new ViewModel(); - string targetNullValue = nameof(BindSpecifiedPropertyWithInlineOneWayConvertAndPositionalParameters) + " null"; - string fallbackValue = nameof(BindSpecifiedPropertyWithInlineOneWayConvertAndPositionalParameters) + " fallback"; + var targetNullValue = nameof(BindSpecifiedPropertyWithInlineOneWayConvertAndPositionalParameters) + " null"; + var fallbackValue = nameof(BindSpecifiedPropertyWithInlineOneWayConvertAndPositionalParameters) + " fallback"; button.Bind( Button.TextProperty, nameof(viewModel.Text), BindingMode.OneWay, - (string text) => $"'{text?.Trim('\'')}'", + (string? text) => $"'{text?.Trim('\'')}'", null, stringFormat, source, @@ -201,17 +201,17 @@ public void BindSpecifiedPropertyWithInlineOneWayConvertAndPositionalParameters( public void BindSpecifiedPropertyWithInlineOneWayParameterizedConvertAndPositionalParameters() { var button = new Button(); - int converterParameter = 1; - string stringFormat = nameof(BindSpecifiedPropertyWithInlineOneWayParameterizedConvertAndPositionalParameters) + " {0}"; + var converterParameter = 1; + var stringFormat = nameof(BindSpecifiedPropertyWithInlineOneWayParameterizedConvertAndPositionalParameters) + " {0}"; object source = new ViewModel(); - string targetNullValue = nameof(BindSpecifiedPropertyWithInlineOneWayParameterizedConvertAndPositionalParameters) + " null"; - string fallbackValue = nameof(BindSpecifiedPropertyWithInlineOneWayParameterizedConvertAndPositionalParameters) + " fallback"; + var targetNullValue = nameof(BindSpecifiedPropertyWithInlineOneWayParameterizedConvertAndPositionalParameters) + " null"; + var fallbackValue = nameof(BindSpecifiedPropertyWithInlineOneWayParameterizedConvertAndPositionalParameters) + " fallback"; button.Bind( Button.TextProperty, nameof(viewModel.Text), BindingMode.OneWay, - (string text, int repeat) => string.Concat(Enumerable.Repeat($"'{text?.Trim('\'')}'", repeat)), + (string? text, int? repeat) => string.Concat(Enumerable.Repeat($"'{text?.Trim('\'')}'", repeat ?? throw new NullReferenceException())), null, converterParameter, stringFormat, @@ -239,17 +239,17 @@ public void BindSpecifiedPropertyWithInlineOneWayParameterizedConvertAndPosition public void BindSpecifiedPropertyWithInlineTwoWayConvertAndPositionalParameters() { var button = new Button(); - string stringFormat = nameof(BindSpecifiedPropertyWithInlineTwoWayConvertAndPositionalParameters) + " {0}"; + var stringFormat = nameof(BindSpecifiedPropertyWithInlineTwoWayConvertAndPositionalParameters) + " {0}"; object source = new ViewModel(); - string targetNullValue = nameof(BindSpecifiedPropertyWithInlineTwoWayConvertAndPositionalParameters) + " null"; - string fallbackValue = nameof(BindSpecifiedPropertyWithInlineTwoWayConvertAndPositionalParameters) + " fallback"; + var targetNullValue = nameof(BindSpecifiedPropertyWithInlineTwoWayConvertAndPositionalParameters) + " null"; + var fallbackValue = nameof(BindSpecifiedPropertyWithInlineTwoWayConvertAndPositionalParameters) + " fallback"; button.Bind( Button.TextProperty, nameof(viewModel.Text), BindingMode.TwoWay, - (string text) => $"'{text?.Trim('\'')}'", - text => text?.Trim('\''), + (string? text) => $"'{text?.Trim('\'')}'", + text => text?.Trim('\'') ?? throw new NullReferenceException(), stringFormat, source, targetNullValue, @@ -274,18 +274,18 @@ public void BindSpecifiedPropertyWithInlineTwoWayConvertAndPositionalParameters( public void BindSpecifiedPropertyWithInlineTwoWayParameterizedConvertAndPositionalParameters() { var button = new Button(); - int converterParameter = 1; - string stringFormat = nameof(BindSpecifiedPropertyWithInlineTwoWayParameterizedConvertAndPositionalParameters) + " {0}"; + var converterParameter = 1; + var stringFormat = nameof(BindSpecifiedPropertyWithInlineTwoWayParameterizedConvertAndPositionalParameters) + " {0}"; object source = new ViewModel(); - string targetNullValue = nameof(BindSpecifiedPropertyWithInlineTwoWayParameterizedConvertAndPositionalParameters) + " null"; - string fallbackValue = nameof(BindSpecifiedPropertyWithInlineTwoWayParameterizedConvertAndPositionalParameters) + " fallback"; + var targetNullValue = nameof(BindSpecifiedPropertyWithInlineTwoWayParameterizedConvertAndPositionalParameters) + " null"; + var fallbackValue = nameof(BindSpecifiedPropertyWithInlineTwoWayParameterizedConvertAndPositionalParameters) + " fallback"; button.Bind( Button.TextProperty, nameof(viewModel.Text), BindingMode.TwoWay, - (string text, int repeat) => string.Concat(Enumerable.Repeat($"'{text?.Trim('\'')}'", repeat)), - (text, repeat) => text?.Substring(0, text.Length / repeat).Trim('\''), + (string? text, int? repeat) => string.Concat(Enumerable.Repeat($"'{text?.Trim('\'')}'", repeat ?? throw new NullReferenceException())), + (text, repeat) => text?.Substring(0, text.Length / repeat ?? throw new NullReferenceException())?.Trim('\'') ?? throw new NullReferenceException(), converterParameter, stringFormat, source, @@ -321,7 +321,7 @@ public void BindDefaultPropertyWithPositionalParameters() { var label = new Label(); object converterParameter = 1; - string stringFormat = nameof(BindDefaultPropertyWithPositionalParameters) + " {0}"; + var stringFormat = nameof(BindDefaultPropertyWithPositionalParameters) + " {0}"; IValueConverter converter = new ToStringConverter(); object source = new ViewModel(); object targetNullValue = nameof(BindDefaultPropertyWithPositionalParameters) + " null"; @@ -358,7 +358,7 @@ public void BindDefaultPropertyWithInlineOneWayConvertAndDefaults() var label = new Label(); label.Bind( nameof(viewModel.Text), - convert: (string text) => $"'{text}'" + convert: (string? text) => $"'{text}'" ); BindingHelpers.AssertBindingExists( @@ -376,7 +376,7 @@ public void BindDefaultPropertyWithInlineOneWayParameterizedConvertAndDefaults() var label = new Label(); label.Bind( nameof(viewModel.Text), - convert: (string text, int repeat) => string.Concat(Enumerable.Repeat($"'{text?.Trim('\'')}'", repeat)), + convert: (string? text, int? repeat) => string.Concat(Enumerable.Repeat($"'{text?.Trim('\'')}'", repeat ?? throw new NullReferenceException())), converterParameter: 1 ); @@ -397,8 +397,8 @@ public void BindDefaultPropertyWithInlineTwoWayConvertAndDefaults() label.Bind( nameof(viewModel.Text), BindingMode.TwoWay, - (string text) => $"'{text?.Trim('\'')}'", - text => text?.Trim('\'') + (string? text) => $"'{text?.Trim('\'')}'", + text => text?.Trim('\'') ?? throw new NullReferenceException() ); BindingHelpers.AssertBindingExists( @@ -418,8 +418,8 @@ public void BindDefaultPropertyWithInlineTwoWayParameterizedConvertAndDefaults() label.Bind( nameof(viewModel.Text), BindingMode.TwoWay, - (string text, int repeat) => string.Concat(Enumerable.Repeat($"'{text?.Trim('\'')}'", repeat)), - (text, repeat) => text?.Substring(0, text.Length / repeat).Trim('\''), + (string? text, int? repeat) => string.Concat(Enumerable.Repeat($"'{text?.Trim('\'')}'", repeat ?? throw new NullReferenceException())), + (text, repeat) => text?.Substring(0, text.Length / repeat ?? throw new NullReferenceException()).Trim('\'') ?? throw new NullReferenceException(), 2 ); @@ -438,15 +438,15 @@ public void BindDefaultPropertyWithInlineTwoWayParameterizedConvertAndDefaults() public void BindDefaultPropertyWithInlineOneWayConvertAndPositionalParameters() { var label = new Label(); - string stringFormat = nameof(BindDefaultPropertyWithInlineOneWayConvertAndPositionalParameters) + " {0}"; + var stringFormat = nameof(BindDefaultPropertyWithInlineOneWayConvertAndPositionalParameters) + " {0}"; object source = new ViewModel(); - string targetNullValue = nameof(BindDefaultPropertyWithInlineOneWayConvertAndPositionalParameters) + " null"; - string fallbackValue = nameof(BindDefaultPropertyWithInlineOneWayConvertAndPositionalParameters) + " fallback"; + var targetNullValue = nameof(BindDefaultPropertyWithInlineOneWayConvertAndPositionalParameters) + " null"; + var fallbackValue = nameof(BindDefaultPropertyWithInlineOneWayConvertAndPositionalParameters) + " fallback"; label.Bind( nameof(viewModel.Text), BindingMode.OneWay, - (string text) => $"'{text?.Trim('\'')}'", + (string? text) => $"'{text?.Trim('\'')}'", null, stringFormat, source, @@ -472,16 +472,16 @@ public void BindDefaultPropertyWithInlineOneWayConvertAndPositionalParameters() public void BindDefaultPropertyWithInlineOneWayParameterizedConvertAndPositionalParameters() { var label = new Label(); - int converterParameter = 1; - string stringFormat = nameof(BindDefaultPropertyWithInlineOneWayParameterizedConvertAndPositionalParameters) + " {0}"; + var converterParameter = 1; + var stringFormat = nameof(BindDefaultPropertyWithInlineOneWayParameterizedConvertAndPositionalParameters) + " {0}"; object source = new ViewModel(); - string targetNullValue = nameof(BindDefaultPropertyWithInlineOneWayParameterizedConvertAndPositionalParameters) + " null"; - string fallbackValue = nameof(BindDefaultPropertyWithInlineOneWayParameterizedConvertAndPositionalParameters) + " fallback"; + var targetNullValue = nameof(BindDefaultPropertyWithInlineOneWayParameterizedConvertAndPositionalParameters) + " null"; + var fallbackValue = nameof(BindDefaultPropertyWithInlineOneWayParameterizedConvertAndPositionalParameters) + " fallback"; label.Bind( nameof(viewModel.Text), BindingMode.OneWay, - (string text, int repeat) => string.Concat(Enumerable.Repeat($"'{text?.Trim('\'')}'", repeat)), + (string? text, int? repeat) => string.Concat(Enumerable.Repeat($"'{text?.Trim('\'')}'", repeat ?? throw new NullReferenceException())), null, converterParameter, stringFormat, @@ -509,16 +509,16 @@ public void BindDefaultPropertyWithInlineOneWayParameterizedConvertAndPositional public void BindDefaultPropertyWithInlineTwoWayConvertAndPositionalParameters() { var label = new Label(); - string stringFormat = nameof(BindDefaultPropertyWithInlineTwoWayConvertAndPositionalParameters) + " {0}"; + var stringFormat = nameof(BindDefaultPropertyWithInlineTwoWayConvertAndPositionalParameters) + " {0}"; object source = new ViewModel(); - string targetNullValue = nameof(BindDefaultPropertyWithInlineTwoWayConvertAndPositionalParameters) + " null"; - string fallbackValue = nameof(BindDefaultPropertyWithInlineTwoWayConvertAndPositionalParameters) + " fallback"; + var targetNullValue = nameof(BindDefaultPropertyWithInlineTwoWayConvertAndPositionalParameters) + " null"; + var fallbackValue = nameof(BindDefaultPropertyWithInlineTwoWayConvertAndPositionalParameters) + " fallback"; label.Bind( nameof(viewModel.Text), BindingMode.TwoWay, - (string text) => $"'{text?.Trim('\'')}'", - text => text?.Trim('\''), + (string? text) => $"'{text?.Trim('\'')}'", + text => text?.Trim('\'') ?? throw new NullReferenceException(), stringFormat, source, targetNullValue, @@ -543,17 +543,17 @@ public void BindDefaultPropertyWithInlineTwoWayConvertAndPositionalParameters() public void BindDefaultPropertyWithInlineTwoWayParameterizedConvertAndPositionalParameters() { var label = new Label(); - int converterParameter = 1; - string stringFormat = nameof(BindDefaultPropertyWithInlineTwoWayParameterizedConvertAndPositionalParameters) + " {0}"; + var converterParameter = 1; + var stringFormat = nameof(BindDefaultPropertyWithInlineTwoWayParameterizedConvertAndPositionalParameters) + " {0}"; object source = new ViewModel(); - string targetNullValue = nameof(BindDefaultPropertyWithInlineTwoWayParameterizedConvertAndPositionalParameters) + " null"; - string fallbackValue = nameof(BindDefaultPropertyWithInlineTwoWayParameterizedConvertAndPositionalParameters) + " fallback"; + var targetNullValue = nameof(BindDefaultPropertyWithInlineTwoWayParameterizedConvertAndPositionalParameters) + " null"; + var fallbackValue = nameof(BindDefaultPropertyWithInlineTwoWayParameterizedConvertAndPositionalParameters) + " fallback"; label.Bind( nameof(viewModel.Text), BindingMode.TwoWay, - (string text, int repeat) => string.Concat(Enumerable.Repeat($"'{text?.Trim('\'')}'", repeat)), - (text, repeat) => text?.Substring(0, text.Length / repeat).Trim('\''), + (string? text, int? repeat) => string.Concat(Enumerable.Repeat($"'{text?.Trim('\'')}'", repeat ?? throw new NullReferenceException())), + (text, repeat) => text?.Substring(0, text.Length / repeat ?? throw new NullReferenceException()).Trim('\'') ?? throw new NullReferenceException(), converterParameter, stringFormat, source, @@ -580,7 +580,7 @@ public void BindDefaultPropertyWithInlineTwoWayParameterizedConvertAndPositional public void BindCommandWithDefaults() { var textCell = new TextCell(); - string path = nameof(viewModel.Command); + var path = nameof(viewModel.Command); textCell.BindCommand(path); @@ -592,7 +592,7 @@ public void BindCommandWithDefaults() public void BindCommandWithoutParameter() { var textCell = new TextCell(); - string path = nameof(viewModel.Command); + var path = nameof(viewModel.Command); textCell.BindCommand(path, parameterPath: null); @@ -605,8 +605,8 @@ public void BindCommandWithPositionalParameters() { var textCell = new TextCell(); object source = new ViewModel(); - string path = nameof(viewModel.Command); - string parameterPath = nameof(viewModel.Id); + var path = nameof(viewModel.Command); + var parameterPath = nameof(viewModel.Id); object parameterSource = new ViewModel(); textCell.BindCommand(path, source, parameterPath, parameterSource); @@ -637,21 +637,21 @@ public void SupportDerivedElements() .Bind(nameof(viewModel.Text)) .Bind( nameof(viewModel.Text), - convert: (string text) => $"'{text}'") + convert: (string? text) => $"'{text}'") .Bind( nameof(viewModel.Text), - convert: (string text, int repeat) => string.Concat(Enumerable.Repeat($"'{text?.Trim('\'')}'", repeat))) + convert: (string? text, int? repeat) => string.Concat(Enumerable.Repeat($"'{text?.Trim('\'')}'", repeat ?? throw new NullReferenceException()))) .Bind( DerivedFromLabel.TextColorProperty, nameof(viewModel.TextColor)) .Bind( DerivedFromLabel.BackgroundColorProperty, nameof(viewModel.IsRed), - convert: (bool isRed) => isRed ? Color.Black : Color.Transparent) + convert: (bool? isRed) => isRed.HasValue && isRed.Value ? Color.Black : Color.Transparent) .Bind( Label.TextColorProperty, nameof(viewModel.IsRed), - convert: (bool isRed, double alpha) => (isRed ? Color.Red : Color.Green).MultiplyAlpha(alpha)) + convert: (bool? isRed, double? alpha) => (isRed.HasValue && isRed.Value ? Color.Red : Color.Green).MultiplyAlpha(alpha ?? throw new NullReferenceException())) .Invoke(l => l.Text = nameof(SupportDerivedElements)) .Assign(out DerivedFromLabel assignDerivedFromLabel)); @@ -664,9 +664,9 @@ class ViewModel { public Guid Id { get; set; } - public ICommand Command { get; set; } + public ICommand? Command { get; set; } - public string Text { get; set; } + public string? Text { get; set; } public Color TextColor { get; set; } diff --git a/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BindableObjectMultiBindExtensionsTests.cs b/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BindableObjectMultiBindExtensionsTests.cs index f23ff75a1..5822b74e3 100644 --- a/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BindableObjectMultiBindExtensionsTests.cs +++ b/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BindableObjectMultiBindExtensionsTests.cs @@ -9,9 +9,9 @@ namespace Xamarin.CommunityToolkit.Markup.UnitTests [TestFixture] public class BindableObjectMultiBindExtensionsTests : MarkupBaseTestFixture { - ViewModel viewModel; - List testBindings; - List testConvertValues; + ViewModel? viewModel; + List? testBindings; + List? testConvertValues; [SetUp] public override void Setup() @@ -20,13 +20,13 @@ public override void Setup() viewModel = new ViewModel(); testBindings = new List - { + { new Binding(nameof(viewModel.Text)), new Binding(nameof(viewModel.Id)), new Binding(nameof(viewModel.IsDone)), new Binding(nameof(viewModel.Fraction)), new Binding(nameof(viewModel.Count)) - }; + }; testConvertValues = new List { @@ -53,16 +53,23 @@ public override void TearDown() [TestCase(true, true)] public void BindSpecifiedPropertyWith2BindingsAndInlineConvert(bool testConvert, bool testConvertBack) { + _ = testBindings ?? throw new NullReferenceException(); + var label = new Label(); // Repeat inline converter code to test that the Bind overloads allow inferring the generic parameter types if (testConvert && testConvertBack) { - label.Bind( + label.Bind( Label.TextProperty, testBindings[0], testBindings[1], - ((string text, Guid id) v) => Format(0, v.text, v.id), - (string formatted) => { var u = Unformat(0, formatted); return (u.Text, u.Id); } + ((string? text, Guid id) v) => Format(0, v.text, v.id), + (string? formatted) => + { + _ = formatted ?? throw new NullReferenceException(); + var u = Unformat(0, formatted); + return (u.Text, u.Id); + } ); } else if (testConvert && !testConvertBack) @@ -70,7 +77,7 @@ public void BindSpecifiedPropertyWith2BindingsAndInlineConvert(bool testConvert, label.Bind( Label.TextProperty, testBindings[0], testBindings[1], - ((string text, Guid id) v) => Format(0, v.text, v.id) + ((string? text, Guid id) v) => Format(0, v.text, v.id) ); } else if (!testConvert && testConvertBack) @@ -78,7 +85,7 @@ public void BindSpecifiedPropertyWith2BindingsAndInlineConvert(bool testConvert, label.Bind( Label.TextProperty, testBindings[0], testBindings[1], - convertBack: (string formatted) => { var u = Unformat(0, formatted); return (u.Text, u.Id); } + convertBack: (string? formatted) => { var u = Unformat(0, formatted ?? throw new NullReferenceException()); return (u.Text, u.Id); } ); } @@ -91,34 +98,36 @@ public void BindSpecifiedPropertyWith2BindingsAndInlineConvert(bool testConvert, [TestCase(true, true)] public void BindSpecifiedPropertyWith2BindingsAndInlineConvertAndParameter(bool testConvert, bool testConvertBack) { + _ = testBindings ?? throw new NullReferenceException(); + var label = new Label(); // Repeat inline converter code to test that the Bind overloads allow inferring the generic parameter types if (testConvert && testConvertBack) { - label.Bind( + label.Bind( Label.TextProperty, testBindings[0], testBindings[1], - ((string text, Guid id) v, int parameter) => Format(parameter, v.text, v.id), - (string formatted, int parameter) => { var u = Unformat(parameter, formatted); return (u.Text, u.Id); }, + ((string? text, Guid id) v, int? parameter) => Format(parameter ?? throw new NullReferenceException(), v.text, v.id), + (string? formatted, int? parameter) => { var u = Unformat(parameter ?? throw new NullReferenceException(), formatted ?? throw new NullReferenceException()); return (u.Text, u.Id); }, converterParameter: 2 ); } else if (testConvert && !testConvertBack) { - label.Bind( + label.Bind( Label.TextProperty, testBindings[0], testBindings[1], - ((string text, Guid id) v, int parameter) => Format(parameter, v.text, v.id), + ((string? text, Guid id) v, int? parameter) => Format(parameter ?? throw new NullReferenceException(), v.text, v.id), converterParameter: 2 ); } else if (!testConvert && testConvertBack) { - label.Bind( + label.Bind( Label.TextProperty, testBindings[0], testBindings[1], - convertBack: (string formatted, int parameter) => { var u = Unformat(parameter, formatted); return (u.Text, u.Id); }, + convertBack: (string? formatted, int? parameter) => { var u = Unformat(parameter ?? throw new NullReferenceException(), formatted ?? throw new NullReferenceException()); return (u.Text, u.Id); }, converterParameter: 2 ); } @@ -132,32 +141,34 @@ public void BindSpecifiedPropertyWith2BindingsAndInlineConvertAndParameter(bool [TestCase(true, true)] public void BindSpecifiedPropertyWith3BindingsAndInlineConvert(bool testConvert, bool testConvertBack) { + _ = testBindings ?? throw new NullReferenceException(); + var label = new Label(); // Repeat inline converter code to test that the Bind overloads allow inferring the generic parameter types if (testConvert && testConvertBack) { - label.Bind( + label.Bind( Label.TextProperty, testBindings[0], testBindings[1], testBindings[2], - ((string text, Guid id, bool isDone) v) => Format(0, v.text, v.id, v.isDone), - (string formatted) => { var u = Unformat(0, formatted); return (u.Text, u.Id, u.IsDone); } + ((string? text, Guid id, bool isDone) v) => Format(0, v.text, v.id, v.isDone), + (string? formatted) => { var u = Unformat(0, formatted ?? throw new NullReferenceException()); return (u.Text, u.Id, u.IsDone); } ); } else if (testConvert && !testConvertBack) { - label.Bind( + label.Bind( Label.TextProperty, testBindings[0], testBindings[1], testBindings[2], - ((string text, Guid id, bool isDone) v) => Format(0, v.text, v.id, v.isDone) + ((string? text, Guid id, bool isDone) v) => Format(0, v.text, v.id, v.isDone) ); } else if (!testConvert && testConvertBack) { - label.Bind( + label.Bind( Label.TextProperty, testBindings[0], testBindings[1], testBindings[2], - convertBack: (string formatted) => { var u = Unformat(0, formatted); return (u.Text, u.Id, u.IsDone); } + convertBack: (string? formatted) => { var u = Unformat(0, formatted ?? throw new NullReferenceException()); return (u.Text, u.Id, u.IsDone); } ); } @@ -170,25 +181,27 @@ public void BindSpecifiedPropertyWith3BindingsAndInlineConvert(bool testConvert, [TestCase(true, true)] public void BindSpecifiedPropertyWith3BindingsAndInlineConvertAndParameter(bool testConvert, bool testConvertBack) { + _ = testBindings ?? throw new NullReferenceException(); + var label = new Label(); // Repeat inline converter code to test that the Bind overloads allow inferring the generic parameter types if (testConvert && testConvertBack) { label.Bind( - Label.TextProperty, - testBindings[0], testBindings[1], testBindings[2], - ((string text, Guid id, bool isDone) v, int parameter) => Format(parameter, v.text, v.id, v.isDone), - (string formatted, int parameter) => { var u = Unformat(parameter, formatted); return (u.Text, u.Id, u.IsDone); }, - converterParameter: 2 - ); + Label.TextProperty, + testBindings[0], testBindings[1], testBindings[2], + ((string? text, Guid id, bool isDone) v, int? parameter) => Format(parameter ?? throw new NullReferenceException(), v.text, v.id, v.isDone), + (string? formatted, int? parameter) => { var u = Unformat(parameter ?? throw new NullReferenceException(), formatted ?? throw new NullReferenceException()); return (u.Text ?? throw new NullReferenceException(), u.Id, u.IsDone); }, + converterParameter: 2 + ); } else if (testConvert && !testConvertBack) { label.Bind( Label.TextProperty, testBindings[0], testBindings[1], testBindings[2], - ((string text, Guid id, bool isDone) v, int parameter) => Format(parameter, v.text, v.id, v.isDone), + ((string? text, Guid id, bool isDone) v, int? parameter) => Format(parameter ?? throw new NullReferenceException(), v.text, v.id, v.isDone), converterParameter: 2 ); } @@ -197,7 +210,7 @@ public void BindSpecifiedPropertyWith3BindingsAndInlineConvertAndParameter(bool label.Bind( Label.TextProperty, testBindings[0], testBindings[1], testBindings[2], - convertBack: (string formatted, int parameter) => { var u = Unformat(parameter, formatted); return (u.Text, u.Id, u.IsDone); }, + convertBack: (string? formatted, int? parameter) => { var u = Unformat(parameter ?? throw new NullReferenceException(), formatted ?? throw new NullReferenceException()); return (u.Text, u.Id, u.IsDone); }, converterParameter: 2 ); } @@ -211,6 +224,8 @@ public void BindSpecifiedPropertyWith3BindingsAndInlineConvertAndParameter(bool [TestCase(true, true)] public void BindSpecifiedPropertyWith4BindingsAndInlineConvert(bool testConvert, bool testConvertBack) { + _ = testBindings ?? throw new NullReferenceException(); + var label = new Label(); // Repeat inline converter code to test that the Bind overloads allow inferring the generic parameter types @@ -219,8 +234,8 @@ public void BindSpecifiedPropertyWith4BindingsAndInlineConvert(bool testConvert, label.Bind( Label.TextProperty, testBindings[0], testBindings[1], testBindings[2], testBindings[3], - ((string text, Guid id, bool isDone, double fraction) v) => Format(0, v.text, v.id, v.isDone, v.fraction), - (string formatted) => { var u = Unformat(0, formatted); return (u.Text, u.Id, u.IsDone, u.Fraction); } + ((string? text, Guid id, bool isDone, double fraction) v) => Format(0, v.text, v.id, v.isDone, v.fraction), + (string? formatted) => { var u = Unformat(0, formatted ?? throw new NullReferenceException()); return (u.Text ?? string.Empty, u.Id, u.IsDone, u.Fraction); } ); } else if (testConvert && !testConvertBack) @@ -228,7 +243,7 @@ public void BindSpecifiedPropertyWith4BindingsAndInlineConvert(bool testConvert, label.Bind( Label.TextProperty, testBindings[0], testBindings[1], testBindings[2], testBindings[3], - ((string text, Guid id, bool isDone, double fraction) v) => Format(0, v.text, v.id, v.isDone, v.fraction) + ((string? text, Guid id, bool isDone, double fraction) v) => Format(0, v.text, v.id, v.isDone, v.fraction) ); } else if (!testConvert && testConvertBack) @@ -236,7 +251,7 @@ public void BindSpecifiedPropertyWith4BindingsAndInlineConvert(bool testConvert, label.Bind( Label.TextProperty, testBindings[0], testBindings[1], testBindings[2], testBindings[3], - convertBack: (string formatted) => { var u = Unformat(0, formatted); return (u.Text, u.Id, u.IsDone, u.Fraction); } + convertBack: (string? formatted) => { var u = Unformat(0, formatted ?? throw new NullReferenceException()); return (u.Text, u.Id, u.IsDone, u.Fraction); } ); } @@ -249,6 +264,8 @@ public void BindSpecifiedPropertyWith4BindingsAndInlineConvert(bool testConvert, [TestCase(true, true)] public void BindSpecifiedPropertyWith4BindingsAndInlineConvertAndParameter(bool testConvert, bool testConvertBack) { + _ = testBindings ?? throw new NullReferenceException(); + var label = new Label(); // Repeat inline converter code to test that the Bind overloads allow inferring the generic parameter types @@ -257,8 +274,8 @@ public void BindSpecifiedPropertyWith4BindingsAndInlineConvertAndParameter(bool label.Bind( Label.TextProperty, testBindings[0], testBindings[1], testBindings[2], testBindings[3], - ((string text, Guid id, bool isDone, double fraction) v, int parameter) => Format(parameter, v.text, v.id, v.isDone, v.fraction), - (string formatted, int parameter) => { var u = Unformat(parameter, formatted); return (u.Text, u.Id, u.IsDone, u.Fraction); }, + ((string? text, Guid id, bool isDone, double fraction) v, int? parameter) => Format(parameter ?? throw new NullReferenceException(), v.text, v.id, v.isDone, v.fraction), + (string? formatted, int? parameter) => { var u = Unformat(parameter ?? throw new NullReferenceException(), formatted ?? throw new NullReferenceException()); return (u.Text ?? string.Empty, u.Id, u.IsDone, u.Fraction); }, converterParameter: 2 ); } @@ -267,7 +284,7 @@ public void BindSpecifiedPropertyWith4BindingsAndInlineConvertAndParameter(bool label.Bind( Label.TextProperty, testBindings[0], testBindings[1], testBindings[2], testBindings[3], - ((string text, Guid id, bool isDone, double fraction) v, int parameter) => Format(parameter, v.text, v.id, v.isDone, v.fraction), + ((string? text, Guid id, bool isDone, double fraction) v, int? parameter) => Format(parameter ?? throw new NullReferenceException(), v.text, v.id, v.isDone, v.fraction), converterParameter: 2 ); } @@ -276,7 +293,7 @@ public void BindSpecifiedPropertyWith4BindingsAndInlineConvertAndParameter(bool label.Bind( Label.TextProperty, testBindings[0], testBindings[1], testBindings[2], testBindings[3], - convertBack: (string formatted, int parameter) => { var u = Unformat(parameter, formatted); return (u.Text, u.Id, u.IsDone, u.Fraction); }, + convertBack: (string? formatted, int? parameter) => { var u = Unformat(parameter ?? throw new NullReferenceException(), formatted ?? throw new NullReferenceException()); return (u.Text, u.Id, u.IsDone, u.Fraction); }, converterParameter: 2 ); } @@ -290,22 +307,24 @@ public void BindSpecifiedPropertyWith4BindingsAndInlineConvertAndParameter(bool [TestCase(true, true)] public void BindSpecifiedPropertyWithMultipleBindings(bool testConvert, bool testConvertBack) { - Func convert = null; + _ = testBindings ?? throw new NullReferenceException(); + + Func? convert = null; if (testConvert) - convert = (object[] v) => Format(0, v[0], v[1], v[2], v[3], v[4]); + convert = (object[] v) => Format(0, v[0], v[1], v[2], v[3], v[4]); - Func convertBack = null; + Func? convertBack = null; if (testConvertBack) { - convertBack = (string formatted) => - { - var u = Unformat(0, formatted); - return new object[] { u.Text, u.Id, u.IsDone, u.Fraction, u.Count }; + convertBack = (string? formatted) => + { + var result = Unformat(0, formatted ?? throw new NullReferenceException()); + return new object[] { result.Text ?? string.Empty, result.Id, result.IsDone, result.Fraction, result.Count }; }; } var converter = new FuncMultiConverter(convert, convertBack); - var label = new Label { } .Bind (Label.TextProperty, GetTestBindings(5), converter); + var label = new Label { }.Bind(Label.TextProperty, GetTestBindings(5), converter); AssertLabelTextMultiBound(label, 5, testConvert, testConvertBack, converter: converter); } @@ -315,25 +334,25 @@ public void BindSpecifiedPropertyWithMultipleBindings(bool testConvert, bool tes [TestCase(true, true)] public void BindSpecifiedPropertyWithMultipleBindingsAndParameter(bool testConvert, bool testConvertBack) { - Func convert = null; + Func? convert = null; if (testConvert) { convert = (object[] v, int parameter) => Format(parameter, v[0], v[1], v[2], v[3], v[4]); } - Func convertBack = null; + Func? convertBack = null; if (testConvertBack) { - convertBack = (string text, int parameter) => + convertBack = (string? text, int parameter) => { - var u = Unformat(parameter, text); - return new object[] { u.Text, u.Id, u.IsDone, u.Fraction, u.Count }; + var u = Unformat(parameter, text ?? throw new NullReferenceException()); + return new object[] { u.Text ?? string.Empty, u.Id, u.IsDone, u.Fraction, u.Count }; }; } - var converter = new FuncMultiConverter(convert, convertBack); - var label = new Label { } .Bind (Label.TextProperty, GetTestBindings(5), converter, 2); + var converter = new FuncMultiConverter(convert, convertBack); + var label = new Label { }.Bind(Label.TextProperty, GetTestBindings(5), converter, 2); AssertLabelTextMultiBound(label, 5, testConvert, testConvertBack, 2, converter); } @@ -341,22 +360,22 @@ public void BindSpecifiedPropertyWithMultipleBindingsAndParameter(bool testConve object[] GetTestConvertValues(int count) => testConvertValues.Take(count).ToArray(); - string PrefixDots(object value, int count) => $"{new string('.', count)}{value}"; + string PrefixDots(object? value, int count) => $"{new string('.', count)}{value}"; string RemoveDots(string text, int count) => text.Substring(count); - string Format(int parameter, params object[] values) + string Format(int parameter, params object?[] values) { - string formatted = $"'{PrefixDots(values[0], parameter)}'"; - for (int i = 1; i < values.Length; i++) + var formatted = $"'{PrefixDots(values[0], parameter)}'"; + for (var i = 1; i < values.Length; i++) formatted += $", '{values[i]}'"; return formatted; } - (string Text, Guid Id, bool IsDone, double Fraction, int Count) Unformat(int parameter, string formatted) + (string? Text, Guid Id, bool IsDone, double Fraction, int Count) Unformat(int parameter, string formatted) { var split = formatted.Split('\''); - int n = split.Length; + var n = split.Length; return ( n > 1 ? RemoveDots(split[1], parameter) : null, @@ -366,10 +385,10 @@ string Format(int parameter, params object[] values) n > 9 ? int.Parse(split[9]) : 0); } - void AssertLabelTextMultiBound(Label label, int nBindings, bool testConvert, bool testConvertBack, int parameter, IMultiValueConverter converter = null) + void AssertLabelTextMultiBound(Label label, int nBindings, bool testConvert, bool testConvertBack, int parameter, IMultiValueConverter? converter = null) { var values = GetTestConvertValues(nBindings); - string expected = Format(parameter, values); + var expected = Format(parameter, values); BindingHelpers.AssertBindingExists( label, @@ -381,10 +400,10 @@ void AssertLabelTextMultiBound(Label label, int nBindings, bool testConvert, boo assertConvert: c => c.AssertConvert(values, parameter, expected, twoWay: testConvert && testConvertBack, backOnly: !testConvert && testConvertBack)); } - void AssertLabelTextMultiBound(Label label, int nBindings, bool testConvert, bool testConvertBack, IMultiValueConverter converter = null) + void AssertLabelTextMultiBound(Label label, int nBindings, bool testConvert, bool testConvertBack, IMultiValueConverter? converter = null) { var values = GetTestConvertValues(nBindings); - string expected = Format(0, values); + var expected = Format(0, values); BindingHelpers.AssertBindingExists( label, @@ -399,7 +418,7 @@ class ViewModel { public Guid Id { get; set; } - public string Text { get; set; } + public string Text { get; set; } = string.Empty; public bool IsDone { get; set; } diff --git a/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BindingHelpers.cs b/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BindingHelpers.cs index b92161d09..1bd826306 100644 --- a/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BindingHelpers.cs +++ b/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/BindingHelpers.cs @@ -8,10 +8,10 @@ namespace Xamarin.CommunityToolkit.Markup.UnitTests { - internal static class BindingHelpers + static class BindingHelpers { - static MethodInfo getContextMethodInfo; - static FieldInfo bindingFieldInfo; + static MethodInfo? getContextMethodInfo; + static FieldInfo? bindingFieldInfo; internal static void AssertBindingExists( BindableObject bindable, @@ -19,13 +19,13 @@ internal static class BindingHelpers string path = ".", BindingMode mode = BindingMode.Default, bool assertConverterInstanceIsAnyNotNull = false, - IValueConverter converter = null, - object converterParameter = null, - string stringFormat = null, - object source = null, - object targetNullValue = default, - object fallbackValue = default, - Action assertConvert = null) + IValueConverter? converter = null, + object? converterParameter = null, + string? stringFormat = null, + object? source = null, + object? targetNullValue = default, + object? fallbackValue = default, + Action? assertConvert = null) => AssertBindingExists( bindable, targetProperty, path, mode, assertConverterInstanceIsAnyNotNull, converter, converterParameter, stringFormat, source, targetNullValue, fallbackValue, assertConvert); @@ -36,12 +36,12 @@ internal static class BindingHelpers string path = ".", BindingMode mode = BindingMode.Default, bool assertConverterInstanceIsAnyNotNull = false, - IValueConverter converter = null, - string stringFormat = null, - object source = null, - TDest targetNullValue = default, - TDest fallbackValue = default, - Action assertConvert = null) + IValueConverter? converter = null, + string? stringFormat = null, + object? source = null, + TDest? targetNullValue = default, + TDest? fallbackValue = default, + Action? assertConvert = null) => AssertBindingExists( bindable, targetProperty, path, mode, assertConverterInstanceIsAnyNotNull, converter, null, stringFormat, source, targetNullValue, fallbackValue, assertConvert); @@ -52,15 +52,15 @@ internal static class BindingHelpers string path = ".", BindingMode mode = BindingMode.Default, bool assertConverterInstanceIsAnyNotNull = false, - IValueConverter converter = null, - TParam converterParameter = default, - string stringFormat = null, - object source = null, - TDest targetNullValue = default, - TDest fallbackValue = default, - Action assertConvert = null) + IValueConverter? converter = null, + TParam? converterParameter = default, + string? stringFormat = null, + object? source = null, + TDest? targetNullValue = default, + TDest? fallbackValue = default, + Action? assertConvert = null) { - var binding = BindingHelpers.GetBinding(bindable, targetProperty); + var binding = GetBinding(bindable, targetProperty) ?? throw new NullReferenceException(); Assert.That(binding, Is.Not.Null); Assert.That(binding.Path, Is.EqualTo(path)); Assert.That(binding.Mode, Is.EqualTo(mode)); @@ -81,13 +81,13 @@ internal static class BindingHelpers BindableObject bindable, BindableProperty targetProperty, IList bindings, - IMultiValueConverter converter = null, + IMultiValueConverter? converter = null, BindingMode mode = BindingMode.Default, bool assertConverterInstanceIsAnyNotNull = false, - string stringFormat = null, - TDest targetNullValue = default, - TDest fallbackValue = default, - Action assertConvert = null) + string? stringFormat = null, + TDest? targetNullValue = default, + TDest? fallbackValue = default, + Action? assertConvert = null) => AssertBindingExists( bindable, targetProperty, bindings, converter, null, mode, assertConverterInstanceIsAnyNotNull, stringFormat, targetNullValue, fallbackValue, assertConvert); @@ -96,16 +96,16 @@ internal static class BindingHelpers BindableObject bindable, BindableProperty targetProperty, IList bindings, - IMultiValueConverter converter = null, - TParam converterParameter = default, + IMultiValueConverter? converter = null, + TParam? converterParameter = default, BindingMode mode = BindingMode.Default, bool assertConverterInstanceIsAnyNotNull = false, - string stringFormat = null, - TDest targetNullValue = default, - TDest fallbackValue = default, - Action assertConvert = null) + string? stringFormat = null, + TDest? targetNullValue = default, + TDest? fallbackValue = default, + Action? assertConvert = null) { - var binding = BindingHelpers.GetMultiBinding(bindable, targetProperty); + var binding = GetMultiBinding(bindable, targetProperty) ?? throw new NullReferenceException(); Assert.That(binding, Is.Not.Null); Assert.That(binding.Bindings.SequenceEqual(bindings), Is.True); Assert.That(binding.Mode, Is.EqualTo(mode)); @@ -121,16 +121,16 @@ internal static class BindingHelpers assertConvert?.Invoke(binding.Converter); } - internal static Binding GetBinding(BindableObject bindable, BindableProperty property) => GetBindingBase(bindable, property) as Binding; + internal static Binding? GetBinding(BindableObject bindable, BindableProperty property) => GetBindingBase(bindable, property) as Binding; - internal static MultiBinding GetMultiBinding(BindableObject bindable, BindableProperty property) => GetBindingBase(bindable, property) as MultiBinding; + internal static MultiBinding? GetMultiBinding(BindableObject bindable, BindableProperty property) => GetBindingBase(bindable, property) as MultiBinding; /// /// Note that we are only testing whether the Markup helpers create the correct bindings, /// we are not testing the binding mechanism itself; this is why it is justified to access /// private binding API's here for testing. /// - internal static BindingBase GetBindingBase(BindableObject bindable, BindableProperty property) + internal static BindingBase? GetBindingBase(BindableObject bindable, BindableProperty property) { if (getContextMethodInfo == null) getContextMethodInfo = typeof(BindableObject).GetMethod("GetContext", BindingFlags.NonPublic | BindingFlags.Instance); @@ -145,24 +145,24 @@ internal static BindingBase GetBindingBase(BindableObject bindable, BindableProp return bindingFieldInfo?.GetValue(context) as BindingBase; } - internal static IValueConverter AssertConvert(this IValueConverter converter, TValue value, object parameter, TConvertedValue expectedConvertedValue, bool twoWay = false, bool backOnly = false, CultureInfo culture = null) + internal static IValueConverter AssertConvert(this IValueConverter converter, TValue value, object? parameter, TConvertedValue expectedConvertedValue, bool twoWay = false, bool backOnly = false, CultureInfo? culture = null) { - Assert.That(converter?.Convert(value, typeof(object), parameter, culture), Is.EqualTo(backOnly ? default(TConvertedValue) : expectedConvertedValue)); - Assert.That(converter?.ConvertBack(expectedConvertedValue, typeof(object), parameter, culture), Is.EqualTo(twoWay || backOnly ? value : default(TValue))); + Assert.That(converter.Convert(value, typeof(object), parameter, culture), Is.EqualTo(backOnly ? default(TConvertedValue) : expectedConvertedValue)); + Assert.That(converter.ConvertBack(expectedConvertedValue, typeof(object), parameter, culture), Is.EqualTo(twoWay || backOnly ? value : default(TValue))); return converter; } - internal static IValueConverter AssertConvert(this IValueConverter converter, TValue value, TConvertedValue expectedConvertedValue, bool twoWay = false, bool backOnly = false, CultureInfo culture = null) + internal static IValueConverter AssertConvert(this IValueConverter converter, TValue value, TConvertedValue expectedConvertedValue, bool twoWay = false, bool backOnly = false, CultureInfo? culture = null) => AssertConvert(converter, value, null, expectedConvertedValue, twoWay: twoWay, backOnly: backOnly, culture: culture); - internal static IMultiValueConverter AssertConvert(this IMultiValueConverter converter, object[] values, object parameter, TConvertedValue expectedConvertedValue, bool twoWay = false, bool backOnly = false, CultureInfo culture = null) + internal static IMultiValueConverter AssertConvert(this IMultiValueConverter converter, object[] values, object? parameter, TConvertedValue expectedConvertedValue, bool twoWay = false, bool backOnly = false, CultureInfo? culture = null) { - Assert.That(converter?.Convert(values, typeof(TConvertedValue), parameter, culture), Is.EqualTo(backOnly ? BindableProperty.UnsetValue : expectedConvertedValue)); - var convertedBackValues = converter?.ConvertBack(expectedConvertedValue, null, parameter, culture); + Assert.That(converter.Convert(values, typeof(TConvertedValue), parameter, culture), Is.EqualTo(backOnly ? BindableProperty.UnsetValue : expectedConvertedValue)); + var convertedBackValues = converter.ConvertBack(expectedConvertedValue, null, parameter, culture); if (twoWay || backOnly) { Assert.That(convertedBackValues.Length, Is.EqualTo(values.Length)); - for (int i = 0; i < values.Length; i++) + for (var i = 0; i < values.Length; i++) Assert.That(convertedBackValues[i], Is.EqualTo(values[i])); } else @@ -170,7 +170,7 @@ internal static IMultiValueConverter AssertConvert(this IMultiV return converter; } - internal static IMultiValueConverter AssertConvert(this IMultiValueConverter converter, object[] values, TConvertedValue expectedConvertedValue, bool twoWay = false, bool backOnly = false, CultureInfo culture = null) + internal static IMultiValueConverter AssertConvert(this IMultiValueConverter converter, object[] values, TConvertedValue expectedConvertedValue, bool twoWay = false, bool backOnly = false, CultureInfo? culture = null) => AssertConvert(converter, values, null, expectedConvertedValue, twoWay, backOnly, culture); } } diff --git a/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/DefaultBindablePropertiesTests.cs b/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/DefaultBindablePropertiesTests.cs index aefdf1c53..860445cef 100644 --- a/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/DefaultBindablePropertiesTests.cs +++ b/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/DefaultBindablePropertiesTests.cs @@ -127,7 +127,7 @@ public void AllBindableElementsInCoreHaveDefaultBindablePropertyOrAreExcluded() foreach (var type in bindableObjectTypes) { - if (excludedTypeReasons.TryGetValue(type, out string exclusionReason)) + if (excludedTypeReasons.TryGetValue(type, out var exclusionReason)) { Console.WriteLine($"Info: no default BindableProperty defined for BindableObject type {type.FullName} because {exclusionReason}"); continue; @@ -137,7 +137,7 @@ public void AllBindableElementsInCoreHaveDefaultBindablePropertyOrAreExcluded() { failMessage.AppendLine(type.FullName); var propertyNames = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly) - .Where(f => f.FieldType == typeof(BindableProperty)).Select(f => f.DeclaringType.Name + "." + f.Name).ToList(); + .Where(f => f.FieldType == typeof(BindableProperty)).Select(f => f?.DeclaringType?.Name + "." + f?.Name).ToList(); if (propertyNames.Count > 0) { failMessage.AppendLine("\tCandidate properties:"); diff --git a/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/ElementExtensionsTests.cs b/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/ElementExtensionsTests.cs index 40ceabbc3..39916b894 100644 --- a/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/ElementExtensionsTests.cs +++ b/src/Markup/Xamarin.CommunityToolkit.Markup.UnitTests/ElementExtensionsTests.cs @@ -1,4 +1,5 @@ -using NUnit.Framework; +using System; +using NUnit.Framework; using Xamarin.Forms; using FontElement = Xamarin.Forms.Label; // TODO: Get rid of this after we have default interface implementation in Forms for IFontElement @@ -7,7 +8,7 @@ namespace Xamarin.CommunityToolkit.Markup.UnitTests [TestFixture] public class ElementExtensionsTests : MarkupBaseTestFixture