using System; using System.Collections.Generic; using System.Linq; using Android.App; using Android.OS; using Cirrious.CrossCore; using Cirrious.CrossCore.Exceptions; using Cirrious.CrossCore.Platform; using Cirrious.MvvmCross.Droid.FullFragging.Fragments; using Cirrious.MvvmCross.Droid.Platform; using Cirrious.MvvmCross.Droid.Views; using Cirrious.MvvmCross.ViewModels; using Cirrious.MvvmCross.Views; using MvxActivity = Cirrious.MvvmCross.Droid.FullFragging.Views.MvxActivity; using Cirrious.CrossCore.Droid.Views; namespace Cirrious.MvvmCross.Droid.FullFragging { public class MvxCachingFragmentActivityBehavior : MvxBaseActivityAdapter { private const string SavedFragmentTypesKey = "__mvxSavedFragmentTypes"; private const string SavedCurrentFragmentsKey = "__mvxSavedCurrentFragments"; private readonly Dictionary _lookup = new Dictionary(); private Dictionary _currentFragments = new Dictionary(); List Fragments { get { return (Activity as MvxActivity).Fragments; } } FragmentManager FragmentManager { get { return Activity.FragmentManager; } } public MvxCachingFragmentActivityBehavior(MvxActivity activity) : base(activity) { } /// /// Register a Fragment to be shown, this should usually be done in OnCreate /// /// Fragment Type /// ViewModel Type /// The tag of the Fragment, it is used to register it with the FragmentManager public void RegisterFragment(string tag) where TViewModel : IMvxViewModel where TFragment : IMvxFragmentView { var fragInfo = new FragmentInfo(tag, typeof(TFragment), typeof(TViewModel)); _lookup.Add(tag, fragInfo); } protected override void EventSourceOnCreateCalled(object sender, CrossCore.Core.MvxValueEventArgs mvxValueEventArgs) { base.EventSourceOnCreateCalled(sender, mvxValueEventArgs); OnPostCreate(mvxValueEventArgs.Value); } protected override void EventSourceOnSaveInstanceStateCalled(object sender, CrossCore.Core.MvxValueEventArgs mvxValueEventArgs) { OnSaveInstanceState(mvxValueEventArgs.Value); base.EventSourceOnSaveInstanceStateCalled(sender, mvxValueEventArgs); } void OnPostCreate(Bundle savedInstanceState) { if (savedInstanceState == null) return; // Gabriel has blown his trumpet. Ressurect Fragments from the dead. RestoreLookupFromSleep(); IMvxJsonConverter serializer; if (!Mvx.TryResolve(out serializer)) { Mvx.Trace( "Could not resolve IMvxNavigationSerializer, it is going to be hard to create ViewModel cache"); return; } RestoreCurrentFragmentsFromBundle(serializer, savedInstanceState); RestoreViewModelsFromBundle(serializer, savedInstanceState); } private static void RestoreViewModelsFromBundle(IMvxJsonConverter serializer, Bundle savedInstanceState) { IMvxSavedStateConverter savedStateConverter; IMvxMultipleViewModelCache viewModelCache; IMvxViewModelLoader viewModelLoader; if (!Mvx.TryResolve(out savedStateConverter)) { Mvx.Trace("Could not resolve IMvxSavedStateConverter, won't be able to convert saved state"); return; } if (!Mvx.TryResolve(out viewModelCache)) { Mvx.Trace("Could not resolve IMvxMultipleViewModelCache, won't be able to convert saved state"); return; } if (!Mvx.TryResolve(out viewModelLoader)) { Mvx.Trace("Could not resolve IMvxViewModelLoader, won't be able to load ViewModel for caching"); return; } // Harder ressurection, just in case we were killed to death. var json = savedInstanceState.GetString(SavedFragmentTypesKey); if (string.IsNullOrEmpty(json)) return; var savedState = serializer.DeserializeObject>(json); foreach (var item in savedState) { var bundle = savedInstanceState.GetBundle(item.Key); if (bundle.IsEmpty) continue; var mvxBundle = savedStateConverter.Read(bundle); var request = MvxViewModelRequest.GetDefaultRequest(item.Value); // repopulate the ViewModel with the SavedState and cache it. var vm = viewModelLoader.LoadViewModel(request, mvxBundle); viewModelCache.Cache(vm); } } private void RestoreCurrentFragmentsFromBundle(IMvxJsonConverter serializer, Bundle savedInstanceState) { var json = savedInstanceState.GetString(SavedCurrentFragmentsKey); var currentFragments = serializer.DeserializeObject>(json); _currentFragments = currentFragments; } private void RestoreLookupFromSleep() { // See if Fragments were just sleeping, and repopulate the _lookup // with references to them. foreach (var fragment in Fragments) { var fragmentType = fragment.GetType(); var lookup = _lookup.Where(x => x.Value.FragmentType == fragmentType); foreach (var item in lookup.Where(item => item.Value != null)) { // reattach fragment to lookup item.Value.CachedFragment = fragment; } } } private Dictionary CreateFragmentTypesDictionary(Bundle outState) { IMvxSavedStateConverter savedStateConverter; if (!Mvx.TryResolve(out savedStateConverter)) { return null; } var typesForKeys = new Dictionary(); foreach (var item in _lookup) { var fragment = item.Value.CachedFragment as IMvxFragmentView; if (fragment == null) continue; var mvxBundle = fragment.CreateSaveStateBundle(); var bundle = new Bundle(); savedStateConverter.Write(bundle, mvxBundle); outState.PutBundle(item.Key, bundle); typesForKeys.Add(item.Key, item.Value.ViewModelType); } return typesForKeys; } void OnSaveInstanceState(Bundle outState) { if (_lookup.Any()) { var typesForKeys = CreateFragmentTypesDictionary(outState); if (typesForKeys == null) return; IMvxJsonConverter ser; if (!Mvx.TryResolve(out ser)) { return; } var json = ser.SerializeObject(typesForKeys); outState.PutString(SavedFragmentTypesKey, json); json = ser.SerializeObject(_currentFragments); outState.PutString(SavedCurrentFragmentsKey, json); } } /// /// Show Fragment with a specific tag at a specific placeholder /// /// The tag for the fragment to lookup /// Where you want to show the Fragment /// Bundle which usually contains a Serialized MvxViewModelRequest public void ShowFragment(string tag, int contentId, Bundle bundle = null) { FragmentInfo fragInfo; _lookup.TryGetValue(tag, out fragInfo); if (fragInfo == null) throw new MvxException("Could not find tag: {0} in cache, you need to register it first.", tag); string currentFragment; _currentFragments.TryGetValue(contentId, out currentFragment); // Only do something if we are not currently showing the tag at the contentId if (IsContentIdCurrentyShowingFragmentWithTag(contentId, tag)) return; var ft = FragmentManager.BeginTransaction(); OnBeforeFragmentChanging(tag, ft); // if there is a Fragment showing on the contentId we want to present at // remove it first. RemoveFragmentIfShowing(ft, contentId); fragInfo.ContentId = contentId; // if we haven't already created a Fragment, do it now if (fragInfo.CachedFragment == null) { fragInfo.CachedFragment = Fragment.Instantiate(Activity, FragmentJavaName(fragInfo.FragmentType), bundle); ft.Add(fragInfo.ContentId, fragInfo.CachedFragment, fragInfo.Tag); } else ft.Attach(fragInfo.CachedFragment); _currentFragments[contentId] = fragInfo.Tag; OnFragmentChanging(tag, ft); ft.Commit(); FragmentManager.ExecutePendingTransactions(); } private bool IsContentIdCurrentyShowingFragmentWithTag(int contentId, string tag) { string currentFragment; _currentFragments.TryGetValue(contentId, out currentFragment); return currentFragment == tag; } private void RemoveFragmentIfShowing(FragmentTransaction ft, int contentId) { var frag = FragmentManager.FindFragmentById(contentId); if (frag == null) return; ft.Detach(frag); _currentFragments.Remove(contentId); } protected virtual string FragmentJavaName(Type fragmentType) { var namespaceText = fragmentType.Namespace ?? ""; if (namespaceText.Length > 0) namespaceText = namespaceText.ToLowerInvariant() + "."; return namespaceText + fragmentType.Name; } public virtual void OnBeforeFragmentChanging(string tag, FragmentTransaction transaction) { } public virtual void OnFragmentChanging(string tag, FragmentTransaction transaction) { } protected class FragmentInfo { public FragmentInfo(string tag, Type fragmentType, Type viewModelType) { Tag = tag; FragmentType = fragmentType; ViewModelType = viewModelType; } public string Tag { get; private set; } public Type FragmentType { get; private set; } public Type ViewModelType { get; private set; } public Fragment CachedFragment { get; set; } public int ContentId { get; set; } } } }