From 2fde7c102d71c6c059bd4cd0034d6cceacfa140b Mon Sep 17 00:00:00 2001 From: Tomasz Cielecki Date: Thu, 27 Jul 2023 13:08:00 +0200 Subject: [PATCH 1/4] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 621ca42d3c..07d080ae90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [9.1.0](https://github.com/MvvmCross/MvvmCross/tree/10.0.0) (2023-07-27) +## [9.1.0](https://github.com/MvvmCross/MvvmCross/tree/9.1.0) (2023-07-27) [Full Changelog](https://github.com/MvvmCross/MvvmCross/compare/9.0.10...9.1.0) @@ -1703,4 +1703,4 @@ ## [Release-3.0.8.1](https://github.com/MvvmCross/MvvmCross/tree/Release-3.0.8.1) (2013-06-09) -\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* \ No newline at end of file +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* From 4cbd6dd7a34202165f13f8e8e6ed4bca30c47867 Mon Sep 17 00:00:00 2001 From: Tomasz Cielecki Date: Tue, 1 Aug 2023 14:28:28 +0200 Subject: [PATCH 2/4] Update CHANGELOG.md Fixes #4660 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07d080ae90..b7c6f831a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,6 @@ - Remove IMvxViewModel\ and IMvxViewModel\ [\#4651](https://github.com/MvvmCross/MvvmCross/issues/4651) - Remove IMvxViewModelResult [\#4652](https://github.com/MvvmCross/MvvmCross/pull/4652) ([Cheesebaron](https://github.com/Cheesebaron)) - Remove ViewModel cache. It can lead to leaks and doesn't provide value [\#4650](https://github.com/MvvmCross/MvvmCross/pull/4650) ([Cheesebaron](https://github.com/Cheesebaron)) -- Move Setup to MvxAndroidApplication +semver:breaking [\#4546](https://github.com/MvvmCross/MvvmCross/pull/4546) ([Cheesebaron](https://github.com/Cheesebaron)) **Implemented enhancements:** @@ -59,6 +58,7 @@ - Mac view is not created until one of presentation attributes set explicitly [\#4572](https://github.com/MvvmCross/MvvmCross/issues/4572) - Android Mvx.IoCProvider.Resolve hitting MvvmCross.Exceptions.MvxIoCResolveException within ListenableWorkers when App is closed [\#4558](https://github.com/MvvmCross/MvvmCross/issues/4558) - \[Mac\] Correctly set view type when applying default presenter attribu… [\#4573](https://github.com/MvvmCross/MvvmCross/pull/4573) ([snechaev](https://github.com/snechaev)) +- Move Setup to MvxAndroidApplication +semver:breaking [\#4546](https://github.com/MvvmCross/MvvmCross/pull/4546) ([Cheesebaron](https://github.com/Cheesebaron)) **Merged pull requests:** From 1850efca823af51f0deb2abaed74a22646168a3b Mon Sep 17 00:00:00 2001 From: Tomasz Cielecki Date: Tue, 1 Aug 2023 15:21:27 +0200 Subject: [PATCH 3/4] Revert "Remove ViewModel cache. It can lead to leaks and doesn't provide value (#4650)" This reverts commit 2bf57322efa3c9c5022f4cee65d8c3c89a1a7ba5. --- MvvmCross/Core/MvxSetup.cs | 16 ++++++ .../Presenters/MvxAndroidViewPresenter.cs | 4 +- .../IMvxAndroidViewModelRequestTranslator.cs | 7 ++- .../Views/MvxActivityViewExtensions.cs | 4 ++ .../Android/Views/MvxAndroidViewsContainer.cs | 42 ++++++++++++++- .../Views/MvxChildViewModelOwnerAdapter.cs | 37 ++++++++++++++ .../Views/MvxChildViewModelOwnerExtensions.cs | 22 +++++++- .../Android/Views/MvxFragmentExtensions.cs | 12 ++++- .../Presenters/MvxWindowsViewPresenter.cs | 2 +- .../IMvxWindowsViewModelRequestTranslator.cs | 6 ++- .../WinUi/Views/MvxWindowsExtensions.cs | 9 ++++ .../Platforms/WinUi/Views/MvxWindowsPage.cs | 27 ++++++++++ .../WinUi/Views/MvxWindowsViewsContainer.cs | 36 ++++++++++++- .../ViewModels/IMvxChildViewModelCache.cs | 25 +++++++++ .../ViewModels/MvxChildViewModelCache.cs | 51 +++++++++++++++++++ 15 files changed, 289 insertions(+), 11 deletions(-) create mode 100644 MvvmCross/Platforms/Android/Views/MvxChildViewModelOwnerAdapter.cs create mode 100644 MvvmCross/ViewModels/IMvxChildViewModelCache.cs create mode 100644 MvvmCross/ViewModels/MvxChildViewModelCache.cs diff --git a/MvvmCross/Core/MvxSetup.cs b/MvvmCross/Core/MvxSetup.cs index ef8be07985..d867dbf2f2 100644 --- a/MvvmCross/Core/MvxSetup.cs +++ b/MvvmCross/Core/MvxSetup.cs @@ -176,6 +176,8 @@ public virtual void InitializeSecondary() InitializeNavigationSerializer(_iocProvider); SetupLog?.Log(LogLevel.Trace, "Setup: InpcInterception start"); InitializeInpcInterception(_iocProvider); + SetupLog?.Log(LogLevel.Trace, "Setup: InpcInterception start"); + InitializeViewModelCache(_iocProvider); SetupLog?.Log(LogLevel.Trace, "Setup: LastChance start"); InitializeLastChance(_iocProvider); SetupLog?.Log(LogLevel.Trace, "Setup: Secondary end"); @@ -200,6 +202,19 @@ protected virtual void InitializeInpcInterception(IMvxIoCProvider iocProvider) // by default no Inpc calls are intercepted } + protected virtual IMvxChildViewModelCache InitializeViewModelCache(IMvxIoCProvider iocProvider) + { + ValidateArguments(iocProvider); + + var cache = CreateViewModelCache(iocProvider); + return cache; + } + + protected virtual IMvxChildViewModelCache CreateViewModelCache(IMvxIoCProvider iocProvider) + { + return iocProvider.Resolve(); + } + protected virtual IMvxSettings InitializeSettings(IMvxIoCProvider iocProvider) { ValidateArguments(iocProvider); @@ -311,6 +326,7 @@ protected virtual void RegisterDefaultSetupDependencies(IMvxIoCProvider iocProvi iocProvider.LazyConstructAndRegisterSingleton(); iocProvider.LazyConstructAndRegisterSingleton(); iocProvider.LazyConstructAndRegisterSingleton(); + iocProvider.LazyConstructAndRegisterSingleton(); iocProvider.RegisterType(); } diff --git a/MvvmCross/Platforms/Android/Presenters/MvxAndroidViewPresenter.cs b/MvvmCross/Platforms/Android/Presenters/MvxAndroidViewPresenter.cs index 262748ca32..6b80fc5b5a 100644 --- a/MvvmCross/Platforms/Android/Presenters/MvxAndroidViewPresenter.cs +++ b/MvvmCross/Platforms/Android/Presenters/MvxAndroidViewPresenter.cs @@ -415,12 +415,12 @@ private bool ChangePagePresentation(MvxPagePresentationHint pagePresentationHint if (request is MvxViewModelInstanceRequest viewModelInstanceRequest) { - var intent = requestTranslator.GetIntentFor( + var intentWithKey = requestTranslator.GetIntentWithKeyFor( viewModelInstanceRequest.ViewModelInstance, viewModelInstanceRequest ); - return intent; + return intentWithKey.intent; } return requestTranslator.GetIntentFor(request); diff --git a/MvvmCross/Platforms/Android/Views/IMvxAndroidViewModelRequestTranslator.cs b/MvvmCross/Platforms/Android/Views/IMvxAndroidViewModelRequestTranslator.cs index e6e8bb520b..39daf1c5ad 100644 --- a/MvvmCross/Platforms/Android/Views/IMvxAndroidViewModelRequestTranslator.cs +++ b/MvvmCross/Platforms/Android/Views/IMvxAndroidViewModelRequestTranslator.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MS-PL license. // See the LICENSE file in the project root for more information. +using System; using Android.Content; using MvvmCross.ViewModels; @@ -10,6 +11,10 @@ namespace MvvmCross.Platforms.Android.Views public interface IMvxAndroidViewModelRequestTranslator { Intent GetIntentFor(MvxViewModelRequest request); - Intent GetIntentFor(IMvxViewModel existingViewModelToUse, MvxViewModelRequest request); + + // Important: if calling GetIntentWithKeyFor then you must later call RemoveSubViewModelWithKey on the returned key + (Intent intent, int key) GetIntentWithKeyFor(IMvxViewModel existingViewModelToUse, MvxViewModelRequest request); + + void RemoveSubViewModelWithKey(int key); } } diff --git a/MvvmCross/Platforms/Android/Views/MvxActivityViewExtensions.cs b/MvvmCross/Platforms/Android/Views/MvxActivityViewExtensions.cs index 4cd19a7121..da3532fb76 100644 --- a/MvvmCross/Platforms/Android/Views/MvxActivityViewExtensions.cs +++ b/MvvmCross/Platforms/Android/Views/MvxActivityViewExtensions.cs @@ -25,6 +25,10 @@ public static void AddEventListeners(this IMvxEventSourceActivity activity) { var bindingAdapter = new MvxBindingActivityAdapter(activity); } + if (activity is IMvxChildViewModelOwner) + { + var childOwnerAdapter = new MvxChildViewModelOwnerAdapter(activity); + } } public static void OnViewCreate(this IMvxAndroidView androidView, Bundle bundle) diff --git a/MvvmCross/Platforms/Android/Views/MvxAndroidViewsContainer.cs b/MvvmCross/Platforms/Android/Views/MvxAndroidViewsContainer.cs index 59b0abe799..d52cff1b53 100644 --- a/MvvmCross/Platforms/Android/Views/MvxAndroidViewsContainer.cs +++ b/MvvmCross/Platforms/Android/Views/MvxAndroidViewsContainer.cs @@ -18,6 +18,7 @@ public class MvxAndroidViewsContainer , IMvxAndroidViewsContainer { private const string ExtrasKey = "MvxLaunchData"; + private const string SubViewModelKey = "MvxSubViewModelKey"; private readonly Context _applicationContext; private readonly ILogger? _logger; @@ -28,6 +29,8 @@ public MvxAndroidViewsContainer(Context applicationContext) _logger = MvxLogHost.GetLog(); } + #region Implementation of IMvxAndroidViewModelRequestTranslator + public virtual IMvxViewModel? Load(Intent intent, IMvxBundle? savedState) { return Load(intent, null, null); @@ -53,6 +56,13 @@ public MvxAndroidViewsContainer(Context applicationContext) return DirectLoad(savedState, viewModelTypeHint); } + IMvxViewModel? mvxViewModel; + if (TryGetEmbeddedViewModel(intent, out mvxViewModel)) + { + _logger?.Log(LogLevel.Trace, "Embedded ViewModel used"); + return mvxViewModel; + } + _logger?.Log(LogLevel.Trace, "Attempting to load new ViewModel from Intent with Extras"); var toReturn = CreateViewModelFromIntent(intent, savedState); if (toReturn != null) @@ -94,6 +104,23 @@ public MvxAndroidViewsContainer(Context applicationContext) return loaderService.LoadViewModel(viewModelRequest, savedState); } + protected virtual bool TryGetEmbeddedViewModel(Intent intent, out IMvxViewModel? mvxViewModel) + { + var embeddedViewModelKey = intent.Extras?.GetInt(SubViewModelKey); + if (embeddedViewModelKey != null && embeddedViewModelKey.Value != 0) + { + mvxViewModel = Mvx.IoCProvider.Resolve().Get(embeddedViewModelKey.Value); + if (mvxViewModel != null) + { + RemoveSubViewModelWithKey(embeddedViewModelKey.Value); + return true; + } + } + + mvxViewModel = null; + return false; + } + public virtual Intent GetIntentFor(MvxViewModelRequest request) { var viewType = GetViewType(request.ViewModelType); @@ -121,13 +148,24 @@ protected virtual void AdjustIntentForPresentation(Intent intent, MvxViewModelRe // intent.AddFlags(ActivityFlags.ClearTop); } - public virtual Intent GetIntentFor(IMvxViewModel existingViewModelToUse, MvxViewModelRequest? request) + public virtual (Intent intent, int key) GetIntentWithKeyFor(IMvxViewModel existingViewModelToUse, MvxViewModelRequest? request) { request ??= MvxViewModelRequest.GetDefaultRequest(existingViewModelToUse.GetType()); var intent = GetIntentFor(request); - return intent; + var childViewModelCache = Mvx.IoCProvider.Resolve(); + var key = childViewModelCache.Cache(existingViewModelToUse); + intent.PutExtra(SubViewModelKey, key); + + return (intent, key); } + + public void RemoveSubViewModelWithKey(int key) + { + Mvx.IoCProvider.Resolve().Remove(key); + } + + #endregion Implementation of IMvxAndroidViewModelRequestTranslator } #nullable restore } diff --git a/MvvmCross/Platforms/Android/Views/MvxChildViewModelOwnerAdapter.cs b/MvvmCross/Platforms/Android/Views/MvxChildViewModelOwnerAdapter.cs new file mode 100644 index 0000000000..b484693e31 --- /dev/null +++ b/MvvmCross/Platforms/Android/Views/MvxChildViewModelOwnerAdapter.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System; +using MvvmCross.Exceptions; +using MvvmCross.Platforms.Android.Views.Base; + +namespace MvvmCross.Platforms.Android.Views +{ + public class MvxChildViewModelOwnerAdapter : MvxBaseActivityAdapter + { + protected IMvxChildViewModelOwner ChildOwner => (IMvxChildViewModelOwner)Activity; + + public MvxChildViewModelOwnerAdapter(IMvxEventSourceActivity eventSource) + : base(eventSource) + { + if (!(eventSource is IMvxChildViewModelOwner)) + { + throw new MvxException("You cannot use a MvxChildViewModelOwnerAdapter on {0}", + eventSource.GetType().Name); + } + } + + protected override void EventSourceOnDestroyCalled(object sender, EventArgs eventArgs) + { + ChildOwner.ClearOwnedSubIndicies(); + base.EventSourceOnDestroyCalled(sender, eventArgs); + } + + protected override void EventSourceOnDisposeCalled(object sender, EventArgs eventArgs) + { + ChildOwner.ClearOwnedSubIndicies(); + base.EventSourceOnDisposeCalled(sender, eventArgs); + } + } +} diff --git a/MvvmCross/Platforms/Android/Views/MvxChildViewModelOwnerExtensions.cs b/MvvmCross/Platforms/Android/Views/MvxChildViewModelOwnerExtensions.cs index e1da297cfa..3cfecd400b 100644 --- a/MvvmCross/Platforms/Android/Views/MvxChildViewModelOwnerExtensions.cs +++ b/MvvmCross/Platforms/Android/Views/MvxChildViewModelOwnerExtensions.cs @@ -28,7 +28,27 @@ public static Intent CreateIntentFor(this IMvxAndroidView view public static Intent CreateIntentFor(this IMvxAndroidView view, MvxViewModelRequest request) { - return Mvx.IoCProvider.Resolve()?.GetIntentFor(request); + return Mvx.IoCProvider.Resolve().GetIntentFor(request); + } + + public static Intent CreateIntentFor(this IMvxChildViewModelOwner view, IMvxViewModel subViewModel) + { + var requestTranslator = Mvx.IoCProvider.Resolve(); + var (intent, key) = requestTranslator.GetIntentWithKeyFor(subViewModel, null); + + view.OwnedSubViewModelIndicies.Add(key); + + return intent; + } + + public static void ClearOwnedSubIndicies(this IMvxChildViewModelOwner view) + { + var translator = Mvx.IoCProvider.Resolve(); + foreach (var ownedSubViewModelIndex in view.OwnedSubViewModelIndicies) + { + translator.RemoveSubViewModelWithKey(ownedSubViewModelIndex); + } + view.OwnedSubViewModelIndicies.Clear(); } } } diff --git a/MvvmCross/Platforms/Android/Views/MvxFragmentExtensions.cs b/MvvmCross/Platforms/Android/Views/MvxFragmentExtensions.cs index efd0ebcb7f..795a6aa819 100644 --- a/MvvmCross/Platforms/Android/Views/MvxFragmentExtensions.cs +++ b/MvvmCross/Platforms/Android/Views/MvxFragmentExtensions.cs @@ -46,15 +46,23 @@ public static Type FindAssociatedViewModelType(this IMvxFragmentView fragmentVie if (viewModelType == null || viewModelType == typeof(IMvxViewModel)) { - MvxLogHost.Default?.Log(LogLevel.Trace, "No ViewModel class specified for {FragmentViewType} in LoadViewModel", + MvxLogHost.Default?.Log(LogLevel.Trace, "No ViewModel class specified for {fragmentViewType} in LoadViewModel", fragmentView.GetType().Name); } if (request == null) request = MvxViewModelRequest.GetDefaultRequest(viewModelType); + var viewModelCache = Mvx.IoCProvider.Resolve(); + if (viewModelCache.Exists(viewModelType)) + { + var viewModelCached = viewModelCache.Get(viewModelType); + viewModelCache.Remove(viewModelType); + return viewModelCached; + } + var loaderService = Mvx.IoCProvider.Resolve(); - var viewModel = loaderService?.LoadViewModel(request, savedState); + var viewModel = loaderService.LoadViewModel(request, savedState); return viewModel; } diff --git a/MvvmCross/Platforms/WinUi/Presenters/MvxWindowsViewPresenter.cs b/MvvmCross/Platforms/WinUi/Presenters/MvxWindowsViewPresenter.cs index 826872f5d6..b561236dd1 100644 --- a/MvvmCross/Platforms/WinUi/Presenters/MvxWindowsViewPresenter.cs +++ b/MvvmCross/Platforms/WinUi/Presenters/MvxWindowsViewPresenter.cs @@ -91,7 +91,7 @@ protected virtual string GetRequestText(MvxViewModelRequest request) string requestText = string.Empty; if (request is MvxViewModelInstanceRequest) { - requestText = requestTranslator.GetRequestTextFor(((MvxViewModelInstanceRequest)request).ViewModelInstance); + requestText = requestTranslator.GetRequestTextWithKeyFor(((MvxViewModelInstanceRequest)request).ViewModelInstance); } else { diff --git a/MvvmCross/Platforms/WinUi/Views/IMvxWindowsViewModelRequestTranslator.cs b/MvvmCross/Platforms/WinUi/Views/IMvxWindowsViewModelRequestTranslator.cs index 9690191996..87f1b7a83a 100644 --- a/MvvmCross/Platforms/WinUi/Views/IMvxWindowsViewModelRequestTranslator.cs +++ b/MvvmCross/Platforms/WinUi/Views/IMvxWindowsViewModelRequestTranslator.cs @@ -11,6 +11,10 @@ public interface IMvxWindowsViewModelRequestTranslator string GetRequestTextFor(MvxViewModelRequest request); // Important: if calling GetRequestTextWithKeyFor then you must later call RemoveSubViewModelWithKey on the returned key - string GetRequestTextFor(IMvxViewModel existingViewModelToUse); + string GetRequestTextWithKeyFor(IMvxViewModel existingViewModelToUse); + + void RemoveSubViewModelWithKey(int key); + + int RequestTextGetKey(string requestText); } } diff --git a/MvvmCross/Platforms/WinUi/Views/MvxWindowsExtensions.cs b/MvvmCross/Platforms/WinUi/Views/MvxWindowsExtensions.cs index 88a424af15..63e6c9c761 100644 --- a/MvvmCross/Platforms/WinUi/Views/MvxWindowsExtensions.cs +++ b/MvvmCross/Platforms/WinUi/Views/MvxWindowsExtensions.cs @@ -27,6 +27,15 @@ public static void OnViewCreate(this IMvxWindowsView storeView, Func 0) + { + var viewModelLoader = Mvx.IoCProvider.Resolve(); + viewModelLoader.RemoveSubViewModelWithKey(key); + } + } + public static bool HasRegionAttribute(this Type view) { var attributes = view diff --git a/MvvmCross/Platforms/WinUi/Views/MvxWindowsPage.cs b/MvvmCross/Platforms/WinUi/Views/MvxWindowsPage.cs index e960ea9f78..e392145dd4 100644 --- a/MvvmCross/Platforms/WinUi/Views/MvxWindowsPage.cs +++ b/MvvmCross/Platforms/WinUi/Views/MvxWindowsPage.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MS-PL license. // See the LICENSE file in the project root for more information. +using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Navigation; @@ -111,6 +114,30 @@ protected override void OnNavigatedFrom(NavigationEventArgs e) base.OnNavigatedFrom(e); var bundle = this.CreateSaveStateBundle(); SaveStateBundle(e, bundle); + + var translator = Mvx.IoCProvider.Resolve(); + + if (e.NavigationMode == NavigationMode.Back) + { + var key = translator.RequestTextGetKey(_reqData); + this.OnViewDestroy(key); + } + else + { + var backstack = Frame.BackStack; + if (backstack.Count > 0) + { + var currentEntry = backstack[backstack.Count - 1]; + var key = translator.RequestTextGetKey(currentEntry.Parameter.ToString()); + if (key == 0) + { + var newParamter = translator.GetRequestTextWithKeyFor(ViewModel); + var entry = new PageStackEntry(currentEntry.SourcePageType, newParamter, currentEntry.NavigationTransitionInfo); + backstack.Remove(currentEntry); + backstack.Add(entry); + } + } + } } private string _pageKey; diff --git a/MvvmCross/Platforms/WinUi/Views/MvxWindowsViewsContainer.cs b/MvvmCross/Platforms/WinUi/Views/MvxWindowsViewsContainer.cs index a672c7ebe5..2f1bde9892 100644 --- a/MvvmCross/Platforms/WinUi/Views/MvxWindowsViewsContainer.cs +++ b/MvvmCross/Platforms/WinUi/Views/MvxWindowsViewsContainer.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MS-PL license. // See the LICENSE file in the project root for more information. +using System.Collections.Generic; using MvvmCross.ViewModels; using MvvmCross.Views; @@ -12,6 +13,7 @@ public class MvxWindowsViewsContainer , IMvxStoreViewsContainer { private const string ExtrasKey = "MvxLaunchData"; + private const string SubViewModelKey = "MvxSubViewModelKey"; public IMvxViewModel Load(string requestText, IMvxBundle savedState) { @@ -21,6 +23,15 @@ public IMvxViewModel Load(string requestText, IMvxBundle savedState) dictionary.TryGetValue(ExtrasKey, out string serializedRequest); var request = converter.Serializer.DeserializeObject(serializedRequest); + if (dictionary.TryGetValue(SubViewModelKey, out string viewModelKey)) + { + var key = int.Parse(viewModelKey); + var viewModel = Mvx.IoCProvider.Resolve().Get(key); + if (savedState != null) + viewModel.ReloadState(savedState); + return viewModel; + } + var loaderService = Mvx.IoCProvider.Resolve(); return loaderService.LoadViewModel(request, savedState); } @@ -37,18 +48,41 @@ public string GetRequestTextFor(MvxViewModelRequest request) return requestText; } - public string GetRequestTextFor(IMvxViewModel existingViewModelToUse) + public string GetRequestTextWithKeyFor(IMvxViewModel existingViewModelToUse) { var returnData = new Dictionary(); var converter = Mvx.IoCProvider.Resolve(); var request = MvxViewModelRequest.GetDefaultRequest(existingViewModelToUse.GetType()); + var key = Mvx.IoCProvider.Resolve().Cache(existingViewModelToUse); returnData.Add(ExtrasKey, converter.Serializer.SerializeObject(request)); + returnData.Add(SubViewModelKey, key.ToString()); var requestText = converter.Serializer.SerializeObject(returnData); return requestText; } + + public void RemoveSubViewModelWithKey(int key) + { + Mvx.IoCProvider.Resolve().Remove(key); + } + + public int RequestTextGetKey(string requestText) + { + var returnValue = 0; + var converter = Mvx.IoCProvider.Resolve(); + var dictionary = converter.Serializer.DeserializeObject>(requestText); + + dictionary.TryGetValue(ExtrasKey, out string serializedRequest); + var request = converter.Serializer.DeserializeObject(serializedRequest); + + if (dictionary.TryGetValue(SubViewModelKey, out string viewModelKey)) + { + returnValue = int.Parse(viewModelKey); + } + return returnValue; + } #endregion Implementation of IMvxWindowsViewModelRequestTranslator } } diff --git a/MvvmCross/ViewModels/IMvxChildViewModelCache.cs b/MvvmCross/ViewModels/IMvxChildViewModelCache.cs new file mode 100644 index 0000000000..ac68306d5c --- /dev/null +++ b/MvvmCross/ViewModels/IMvxChildViewModelCache.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace MvvmCross.ViewModels +{ +#nullable enable + public interface IMvxChildViewModelCache + { + int Cache(IMvxViewModel viewModel); + + IMvxViewModel? Get(int index); + + IMvxViewModel? Get(Type viewModelType); + + void Remove(int index); + + void Remove(Type viewModelType); + + bool Exists(Type viewModelType); + } +#nullable restore +} diff --git a/MvvmCross/ViewModels/MvxChildViewModelCache.cs b/MvvmCross/ViewModels/MvxChildViewModelCache.cs new file mode 100644 index 0000000000..8b0bc03f88 --- /dev/null +++ b/MvvmCross/ViewModels/MvxChildViewModelCache.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MS-PL license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MvvmCross.ViewModels +{ +#nullable enable + public class MvxChildViewModelCache : IMvxChildViewModelCache + { + private readonly Dictionary _viewModels = new Dictionary(); + private int _unique = 1; + + public int Cache(IMvxViewModel viewModel) + { + var index = _unique++; + _viewModels[index] = viewModel; + return index; + } + + public bool Exists(Type viewModelType) + { + return _viewModels.Values.Any(x => x.GetType() == viewModelType); + } + + public IMvxViewModel Get(int index) + { + _viewModels.TryGetValue(index, out IMvxViewModel viewModel); + return viewModel; + } + + public IMvxViewModel Get(Type viewModelType) + { + return _viewModels.Values.FirstOrDefault(x => x.GetType() == viewModelType); + } + + public void Remove(int index) + { + _viewModels.Remove(index); + } + + public void Remove(Type viewModelType) + { + _viewModels.Remove(_viewModels.FirstOrDefault(x => x.Value.GetType() == viewModelType).Key); + } + } +#nullable restore +} From b351cfddc0f75f8691c6401c31dd844c8982a4a0 Mon Sep 17 00:00:00 2001 From: Tomasz Cielecki Date: Tue, 1 Aug 2023 17:18:00 +0200 Subject: [PATCH 4/4] Use WeakRef for Single ViewModel Cache (#4661) * Make reference to cached VM weak * Navigate to CollectionView with parameters * Fix sample ditching FFImageLoading and using Glide * No need for this check * Fix colors not showing on collection view sample --- Directory.Packages.props | 3 ++ .../Android/Views/IMvxSingleViewModelCache.cs | 5 +-- .../Android/Views/MvxSingleViewModelCache.cs | 39 ++++++++-------- .../ViewModels/Issues/CollectionViewModel.cs | 27 ++++++----- .../ViewModels/RootViewModel.cs | 2 +- .../Activities/CollectionView.cs | 2 - .../Activities/GlideImageView.cs | 45 +++++++++++++++++++ .../Playground.Droid/Playground.Droid.csproj | 5 ++- .../Resources/layout/itemtemplate_cat.xml | 2 +- .../Resources/layout/itemtemplate_dog.xml | 2 +- .../Resources/layout/itemtemplate_monkey.xml | 2 +- Projects/Playground/Playground.Droid/Setup.cs | 12 ++++- 12 files changed, 102 insertions(+), 44 deletions(-) create mode 100644 Projects/Playground/Playground.Droid/Activities/GlideImageView.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 53244c0e94..b055c336b0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,10 +14,13 @@ + + + diff --git a/MvvmCross/Platforms/Android/Views/IMvxSingleViewModelCache.cs b/MvvmCross/Platforms/Android/Views/IMvxSingleViewModelCache.cs index 4aee817bb5..78fa191a5a 100644 --- a/MvvmCross/Platforms/Android/Views/IMvxSingleViewModelCache.cs +++ b/MvvmCross/Platforms/Android/Views/IMvxSingleViewModelCache.cs @@ -1,8 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MS-PL license. // See the LICENSE file in the project root for more information. - -using Android.OS; +#nullable enable using MvvmCross.ViewModels; namespace MvvmCross.Platforms.Android.Views @@ -11,6 +10,6 @@ public interface IMvxSingleViewModelCache { void Cache(IMvxViewModel toCache, Bundle bundle); - IMvxViewModel GetAndClear(Bundle bundle); + IMvxViewModel? GetAndClear(Bundle bundle); } } diff --git a/MvvmCross/Platforms/Android/Views/MvxSingleViewModelCache.cs b/MvvmCross/Platforms/Android/Views/MvxSingleViewModelCache.cs index 799a78c69d..cf42938893 100644 --- a/MvvmCross/Platforms/Android/Views/MvxSingleViewModelCache.cs +++ b/MvvmCross/Platforms/Android/Views/MvxSingleViewModelCache.cs @@ -1,8 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MS-PL license. // See the LICENSE file in the project root for more information. - -using Android.OS; +#nullable enable using MvvmCross.ViewModels; namespace MvvmCross.Platforms.Android.Views @@ -14,32 +13,36 @@ public class MvxSingleViewModelCache private int _counter; - private IMvxViewModel _currentViewModel; + private WeakReference? _currentViewModel; public void Cache(IMvxViewModel toCache, Bundle bundle) { - _currentViewModel = toCache; + _currentViewModel = new WeakReference(toCache); _counter++; - if (_currentViewModel == null) - { - return; - } - bundle.PutInt(BundleCacheKey, _counter); } - public IMvxViewModel GetAndClear(Bundle bundle) + public IMvxViewModel? GetAndClear(Bundle? bundle) { - var storedViewModel = _currentViewModel; - _currentViewModel = null; - - if (bundle == null) - return null; + try + { + if (bundle == null) + return null; + + if (_currentViewModel?.TryGetTarget(out var storedViewModel) == true) + { + var key = bundle.GetInt(BundleCacheKey); + var toReturn = key == _counter ? storedViewModel : null; + return toReturn; + } + } + finally + { + _currentViewModel = null; + } - var key = bundle.GetInt(BundleCacheKey); - var toReturn = key == _counter ? storedViewModel : null; - return toReturn; + return null; } } } diff --git a/Projects/Playground/Playground.Core/ViewModels/Issues/CollectionViewModel.cs b/Projects/Playground/Playground.Core/ViewModels/Issues/CollectionViewModel.cs index 10080f822f..93d86bd923 100644 --- a/Projects/Playground/Playground.Core/ViewModels/Issues/CollectionViewModel.cs +++ b/Projects/Playground/Playground.Core/ViewModels/Issues/CollectionViewModel.cs @@ -7,12 +7,13 @@ namespace Playground.Core.ViewModels { - public class CollectionViewModel : MvxViewModel + public record CollectionViewParameter(int InitialCount = 40); + + public class CollectionViewModel : MvxViewModel { private readonly Random _random; - public MvxObservableCollection Animals { get; } - = new MvxObservableCollection(); + public MvxObservableCollection Animals { get; } = new(); public MvxCommand DeleteAnimalCommand { get; } public MvxCommand AddAnimalCommand { get; } @@ -25,12 +26,14 @@ public CollectionViewModel() DeleteAnimalCommand = new MvxCommand(DoDeleteAnimalCommand); AddAnimalCommand = new MvxCommand(DoAddAnimalCommand); MarkFavoriteCommand = new MvxCommand(DoMarkFavoriteCommand); - - DoAddAnimalCommand(40); } - private string[] _catImageUrls = new[] + public override void Prepare(CollectionViewParameter parameter) { + DoAddAnimalCommand(parameter.InitialCount); + } + + private readonly string[] _catImageUrls = { "https://loremflickr.com/320/240/cat", "https://www.hillspet.com/content/dam/cp-sites/hills/hills-pet/en_us/exported/cat-care/new-pet-parent/images/mother-cat-and-kitten-sleeping.jpg", "https://www.hillspet.com/content/dam/cp-sites/hills/hills-pet/en_us/exported/cat-care/new-pet-parent/images/calico-kitten-hiding-under-chair.jpg", @@ -41,8 +44,7 @@ public CollectionViewModel() "https://www.petmd.com/sites/default/files/petmd-kitten-facts.jpg" }; - private string[] _dogImageUrls = new[] - { + private readonly string[] _dogImageUrls = { "https://loremflickr.com/320/240/dog", "https://i.imgur.com/KSftE11.jpg", "https://www.thekennelclub.org.uk/media/220388/puppy_environment_alison_spiers.jpg", @@ -52,8 +54,7 @@ public CollectionViewModel() "https://www.fomobones.com/blog/wp-content/uploads/2018/12/puppy-grows-out.jpg" }; - private string[] _monkeyImageUrls = new[] - { + private readonly string[] _monkeyImageUrls = { "https://loremflickr.com/320/240/monkey", "https://allthatsinteresting.com/wordpress/wp-content/uploads/2019/04/baby-primate.jpg", "https://images.law.com/contrib/content/uploads/sites/403/2018/04/monkey-selfie-Article-201804131946.jpg", @@ -64,15 +65,13 @@ public CollectionViewModel() "https://heritagecorridoraletrail.com/wp-content/uploads/2018/01/babymonkey.jpg" }; - private string[] _names = new[] - { + private readonly string[] _names = { "Martijn", "Nicolas", "Nick", "Tomasz", "Stuart", "Marc", "Jeremy", "Jonathan", "Maurits", "Kerry", "Will", "Garfield", "Chris", "Przemyslaw", "Guillaume", "Trevor", "Mihal", "Sylvain", "Andres", "Erik", "Daniel", "Aaron", "Emmanuel", "Iain", "Martin" }; - private Color[] _colors = new[] - { + private readonly Color[] _colors = { Color.Yellow, Color.Green, Color.Blue, Color.Red, Color.Brown, Color.Gold, Color.Orange, Color.Purple, Color.Teal, Color.Pink, Color.Azure, Color.Crimson, Color.Cyan, Color.Gray, Color.Silver, diff --git a/Projects/Playground/Playground.Core/ViewModels/RootViewModel.cs b/Projects/Playground/Playground.Core/ViewModels/RootViewModel.cs index c621c1f6a1..01db074ae2 100644 --- a/Projects/Playground/Playground.Core/ViewModels/RootViewModel.cs +++ b/Projects/Playground/Playground.Core/ViewModels/RootViewModel.cs @@ -81,7 +81,7 @@ public RootViewModel(ILoggerFactory logProvider, IMvxNavigationService navigatio await NavigationService.Navigate()); ShowCollectionViewCommand = - new MvxAsyncCommand(() => NavigationService.Navigate()); + new MvxAsyncCommand(() => NavigationService.Navigate(new CollectionViewParameter(50))); ShowSharedElementsCommand = new MvxAsyncCommand(async () => await NavigationService.Navigate()); diff --git a/Projects/Playground/Playground.Droid/Activities/CollectionView.cs b/Projects/Playground/Playground.Droid/Activities/CollectionView.cs index b8f04afd32..e1f2eac4e2 100644 --- a/Projects/Playground/Playground.Droid/Activities/CollectionView.cs +++ b/Projects/Playground/Playground.Droid/Activities/CollectionView.cs @@ -1,5 +1,3 @@ -using Android.App; -using Android.OS; using MvvmCross.Platforms.Android.Presenters.Attributes; using MvvmCross.Platforms.Android.Views; using Playground.Core.ViewModels; diff --git a/Projects/Playground/Playground.Droid/Activities/GlideImageView.cs b/Projects/Playground/Playground.Droid/Activities/GlideImageView.cs new file mode 100644 index 0000000000..773d0470e1 --- /dev/null +++ b/Projects/Playground/Playground.Droid/Activities/GlideImageView.cs @@ -0,0 +1,45 @@ +using Android.Content; +using Android.Runtime; +using Android.Util; +using Bumptech.Glide; + +namespace Playground.Droid.Activities; + +[Register("playground.droid.GlideImageView")] +public sealed class GlideImageView : ImageView +{ + private string _imagePath; + + public string ImagePath + { + get => _imagePath; + set + { + if (value == null) + return; + + _imagePath = value; + Glide.With(Context).Load(_imagePath).Into(this); + } + } + + public GlideImageView(IntPtr javaReference, JniHandleOwnership transfer) : base(javaReference, transfer) + { + } + + public GlideImageView(Context context) : base(context) + { + } + + public GlideImageView(Context context, IAttributeSet attrs) : base(context, attrs) + { + } + + public GlideImageView(Context context, IAttributeSet attrs, int defStyleAttr) : base(context, attrs, defStyleAttr) + { + } + + public GlideImageView(Context context, IAttributeSet attrs, int defStyleAttr, int defStyleRes) : base(context, attrs, defStyleAttr, defStyleRes) + { + } +} diff --git a/Projects/Playground/Playground.Droid/Playground.Droid.csproj b/Projects/Playground/Playground.Droid/Playground.Droid.csproj index 80d50cad49..95f9546a94 100644 --- a/Projects/Playground/Playground.Droid/Playground.Droid.csproj +++ b/Projects/Playground/Playground.Droid/Playground.Droid.csproj @@ -10,10 +10,11 @@ + + - - + diff --git a/Projects/Playground/Playground.Droid/Resources/layout/itemtemplate_cat.xml b/Projects/Playground/Playground.Droid/Resources/layout/itemtemplate_cat.xml index da153727d7..1c864fde12 100644 --- a/Projects/Playground/Playground.Droid/Resources/layout/itemtemplate_cat.xml +++ b/Projects/Playground/Playground.Droid/Resources/layout/itemtemplate_cat.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="80dp"> - - - (); + } + protected override ILoggerProvider CreateLogProvider() { return new SerilogLoggerProvider(); @@ -41,7 +50,8 @@ protected override ILoggerFactory CreateLogFactory() { Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() - .WriteTo.AndroidLog() + .WriteTo.Async(a => a.AndroidLog()) + .WriteTo.Async(a => a.Trace()) .CreateLogger(); return new SerilogLoggerFactory();