diff --git a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs index 8a661682..15609ec5 100644 --- a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs @@ -23,7 +23,6 @@ using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Synchronization; using Wino.Core.ViewModels; using Wino.Messaging.Client.Calendar; -using Wino.Messaging.Client.Navigation; using Wino.Messaging.Server; using Wino.Messaging.UI; @@ -33,7 +32,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, ICalendarShellClient, IRecipient, IRecipient, - IRecipient, IRecipient, IRecipient { @@ -94,6 +92,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, // For updating account calendars asynchronously. private SemaphoreSlim _accountCalendarUpdateSemaphoreSlim = new(1); + private bool _runtimeSubscriptionsAttached; public CalendarAppShellViewModel(IPreferencesService preferencesService, IStatePersistanceService statePersistanceService, @@ -101,25 +100,24 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, ICalendarService calendarService, IAccountCalendarStateService accountCalendarStateService, INavigationService navigationService, + CalendarPageViewModel calendarPageViewModel, IMailDialogService dialogService, IUpdateManager updateManager, IStoreUpdateService storeUpdateService) { _accountService = accountService; _calendarService = calendarService; + _calendarPageViewModel = calendarPageViewModel; _dialogService = dialogService; _updateManager = updateManager; _storeUpdateService = storeUpdateService; AccountCalendarStateService = accountCalendarStateService; - AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested; - AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged; NavigationService = navigationService; PreferencesService = preferencesService; StatePersistenceService = statePersistanceService; - StatePersistenceService.StatePropertyChanged += PrefefencesChanged; } protected override void OnDispatcherAssigned() @@ -157,9 +155,9 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, public override async void OnNavigatedTo(NavigationMode mode, object parameters) { base.OnNavigatedTo(mode, parameters); + AttachRuntimeSubscriptions(); var activationContext = parameters as ShellModeActivationContext; - var isModeResetActivation = activationContext != null; var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true; PreferencesService.PreferenceChanged -= PreferencesServiceChanged; @@ -167,18 +165,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, await RefreshFooterItemsAsync(mode == NavigationMode.New); - // Preserve the existing calendar shell frame state when the user switches - // between Mail and Calendar modes. Back/forward restoration should not - // force a new CalendarPage navigation, otherwise pages like - // CalendarEventComposePage get dropped from the inner frame stack. - if (mode != NavigationMode.New && !isModeResetActivation) - { - UpdateDateNavigationHeaderItems(); - await InitializeAccountCalendarsAsync(); - ValidateConfiguredNewEventCalendar(); - return; - } - UpdateDateNavigationHeaderItems(); await InitializeAccountCalendarsAsync(); @@ -196,7 +182,38 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, { base.OnNavigatedFrom(mode, parameters); + DetachRuntimeSubscriptions(); PreferencesService.PreferenceChanged -= PreferencesServiceChanged; + _ = ExecuteUIThread(() => + { + DateNavigationHeaderItems.Clear(); + AccountCalendarStateService.ClearGroupedAccountCalendars(); + HighlightedDateRange = null; + SelectedDateNavigationHeaderIndex = -1; + }); + _calendarPageViewModel.CleanupForShellDeactivation(); + } + + private void AttachRuntimeSubscriptions() + { + if (_runtimeSubscriptionsAttached) + return; + + AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested; + AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged; + StatePersistenceService.StatePropertyChanged += PrefefencesChanged; + _runtimeSubscriptionsAttached = true; + } + + private void DetachRuntimeSubscriptions() + { + if (!_runtimeSubscriptionsAttached) + return; + + AccountCalendarStateService.AccountCalendarSelectionStateChanged -= UpdateAccountCalendarRequested; + AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged -= AccountCalendarStateCollectivelyChanged; + StatePersistenceService.StatePropertyChanged -= PrefefencesChanged; + _runtimeSubscriptionsAttached = false; } private async Task ShowWhatIsNewIfNeededAsync() @@ -346,6 +363,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, private DateTime? _navigationDate; private readonly IAccountService _accountService; private readonly ICalendarService _calendarService; + private readonly CalendarPageViewModel _calendarPageViewModel; private readonly IMailDialogService _dialogService; private readonly IUpdateManager _updateManager; private readonly IStoreUpdateService _storeUpdateService; @@ -455,7 +473,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, Messenger.Register(this); Messenger.Register(this); - Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); } @@ -466,7 +483,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, Messenger.Unregister(this); Messenger.Unregister(this); - Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); } @@ -558,8 +574,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, public async void Receive(CalendarEnableStatusChangedMessage message) => await ExecuteUIThread(() => IsCalendarEnabled = message.IsEnabled); - public void Receive(NavigateManageAccountsRequested message) => NavigationService.Navigate(WinoPage.ManageAccountsPage); - public void Receive(CalendarDisplayTypeChangedMessage message) { OnPropertyChanged(nameof(IsVerticalCalendar)); diff --git a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs index 10545017..261b06d1 100644 --- a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Diagnostics; @@ -20,9 +21,9 @@ using Wino.Core.Domain.Entities.Calendar; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models; using Wino.Core.Domain.Models.Calendar; using Wino.Core.Domain.Models.Calendar.CalendarTypeStrategies; -using Wino.Core.Domain.Models; using Wino.Core.Domain.Models.Navigation; using Wino.Core.ViewModels; using Wino.Messaging.Client.Calendar; @@ -37,7 +38,8 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, IRecipient, IRecipient, IRecipient, - IRecipient + IRecipient, + IDisposable { #region Quick Event Creation @@ -145,6 +147,9 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, private SemaphoreSlim _calendarLoadingSemaphore = new(1); private bool isLoadMoreBlocked = false; + private bool _subscriptionsAttached; + private CancellationTokenSource _pageLifetimeCts = new(); + private long _pageLifetimeVersion; [ObservableProperty] private CalendarSettings _currentSettings; @@ -173,11 +178,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, _winoRequestDelegator = winoRequestDelegator; _dialogService = dialogService; - AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested; - AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged; - - // We don't register on navigation here. This page is cached. - RegisterRecipients(); } public override async Task KeyboardShortcutHook(KeyboardShortcutTriggerDetails args) @@ -228,15 +228,35 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, Messenger.Register(this); } + protected override void UnregisterRecipients() + { + base.UnregisterRecipients(); + + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + Messenger.Unregister(this); + } + private void AccountCalendarStateCollectivelyChanged(object sender, GroupedAccountCalendarViewModel e) - => FilterActiveCalendars(DayRanges); + => _ = FilterActiveCalendarsAsync(DayRanges); private void UpdateAccountCalendarRequested(object sender, AccountCalendarViewModel e) - => FilterActiveCalendars(DayRanges); + => _ = FilterActiveCalendarsAsync(DayRanges); - private async void FilterActiveCalendars(IEnumerable dayRangeRenderModels) + private async Task FilterActiveCalendarsAsync(IEnumerable dayRangeRenderModels) { - await ExecuteUIThread(() => + await FilterActiveCalendarsAsync(dayRangeRenderModels, CurrentPageLifetimeVersion).ConfigureAwait(false); + } + + private async Task FilterActiveCalendarsAsync(IEnumerable dayRangeRenderModels, long lifetimeVersion) + { + if (dayRangeRenderModels == null || !IsPageActive(lifetimeVersion)) + return; + + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => { var days = dayRangeRenderModels.SelectMany(a => a.CalendarDays); @@ -269,10 +289,15 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, public override void OnNavigatedTo(NavigationMode mode, object parameters) { + ResetPageLifetime(); + base.OnNavigatedTo(mode, parameters); + AttachSubscriptions(); RefreshSettings(); + IsCalendarEnabled = true; - if (mode == NavigationMode.Back) + if (mode == NavigationMode.Back && DayRanges.Count > 0) { + RestoreVisibleState(); _ = RefreshVisibleRangesAsync(); return; } @@ -283,8 +308,197 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, public override void OnNavigatedFrom(NavigationMode mode, object parameters) { - // CalendarPage is cached and should continue processing calendar item messages - // while details/compose pages are active on top of it. + base.OnNavigatedFrom(mode, parameters); + + if (StatePersistanceService.ApplicationMode == WinoApplicationMode.Calendar) + { + CancelPendingOperations(); + DetachSubscriptions(); + return; + } + + CleanupForShellDeactivation(); + } + + private void AttachSubscriptions() + { + if (_subscriptionsAttached) + return; + + AccountCalendarStateService.AccountCalendarSelectionStateChanged += UpdateAccountCalendarRequested; + AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged += AccountCalendarStateCollectivelyChanged; + _subscriptionsAttached = true; + } + + private void DetachSubscriptions() + { + if (!_subscriptionsAttached) + return; + + AccountCalendarStateService.AccountCalendarSelectionStateChanged -= UpdateAccountCalendarRequested; + AccountCalendarStateService.CollectiveAccountGroupSelectionStateChanged -= AccountCalendarStateCollectivelyChanged; + _subscriptionsAttached = false; + } + + private void ReleasePageState() + { + DetachSubscriptions(); + + DisplayDetailsCalendarItemViewModel = null; + SelectedQuickEventAccountCalendar = null; + SelectedQuickEventDate = null; + SelectedDayRange = null; + SelectedDateRangeIndex = 0; + IsQuickEventDialogOpen = false; + DayRanges = []; + HourSelectionStrings = []; + } + + public void Dispose() + { + CleanupForShellDeactivation(); + } + + public void CleanupForShellDeactivation() + { + CancelPendingOperations(); + ReleasePageState(); + GC.SuppressFinalize(this); + } + + public bool RestoreVisibleState() + { + IsCalendarEnabled = true; + + if (DayRanges.Count == 0) + { + SelectedDayRange = null; + SelectedDateRangeIndex = -1; + return false; + } + + var targetIndex = SelectedDateRangeIndex; + + if (SelectedDayRange != null) + { + var existingSelectedRangeIndex = DayRanges.IndexOf(SelectedDayRange); + if (existingSelectedRangeIndex >= 0) + { + targetIndex = existingSelectedRangeIndex; + } + } + + if (targetIndex < 0 || targetIndex >= DayRanges.Count) + { + targetIndex = 0; + } + + SelectedDateRangeIndex = targetIndex; + SelectedDayRange = DayRanges[targetIndex]; + + return true; + } + + public DateTime GetRestoreDate() + { + if (SelectedDayRange != null) + { + return SelectedDayRange.CalendarRenderOptions.DateRange.StartDate; + } + + if (DayRanges.Count == 0) + { + return DateTime.Now.Date; + } + + var targetIndex = SelectedDateRangeIndex; + if (targetIndex < 0 || targetIndex >= DayRanges.Count) + { + targetIndex = 0; + } + + return DayRanges[targetIndex].CalendarRenderOptions.DateRange.StartDate; + } + + private long CurrentPageLifetimeVersion => Interlocked.Read(ref _pageLifetimeVersion); + + private bool IsPageActive(long lifetimeVersion) + => lifetimeVersion == CurrentPageLifetimeVersion && !_pageLifetimeCts.IsCancellationRequested; + + private bool IsCurrentPageActive => !_pageLifetimeCts.IsCancellationRequested; + + private void ResetPageLifetime() + { + CancelPendingOperations(); + _pageLifetimeCts = new CancellationTokenSource(); + Interlocked.Increment(ref _pageLifetimeVersion); + } + + private void CancelPendingOperations() + { + if (!_pageLifetimeCts.IsCancellationRequested) + { + _pageLifetimeCts.Cancel(); + } + } + + private async Task WaitForCalendarLoadingLockAsync(long lifetimeVersion) + { + if (!IsPageActive(lifetimeVersion)) + return false; + + var cancellationToken = _pageLifetimeCts.Token; + + try + { + await _calendarLoadingSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + return IsPageActive(lifetimeVersion); + } + catch (OperationCanceledException) + { + return false; + } + catch (ObjectDisposedException) + { + return false; + } + } + + private void ReleaseCalendarLoadingLock() + { + try + { + _calendarLoadingSemaphore.Release(); + } + catch (ObjectDisposedException) + { + } + catch (SemaphoreFullException) + { + } + } + + private async Task ExecuteUIThreadIfActiveAsync(long lifetimeVersion, Action action) + { + if (action == null || !IsPageActive(lifetimeVersion)) + return; + + try + { + await ExecuteUIThread(() => + { + if (IsPageActive(lifetimeVersion)) + { + action(); + } + }).ConfigureAwait(false); + } + catch (COMException) when (!IsPageActive(lifetimeVersion)) + { + } + catch (ObjectDisposedException) when (!IsPageActive(lifetimeVersion)) + { + } } [RelayCommand] @@ -492,21 +706,27 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, public async void Receive(LoadCalendarMessage message) { - await _calendarLoadingSemaphore.WaitAsync(); + var lifetimeVersion = CurrentPageLifetimeVersion; + var hasLoadingLock = await WaitForCalendarLoadingLockAsync(lifetimeVersion).ConfigureAwait(false); + + if (!hasLoadingLock) + return; try { - await ExecuteUIThread(() => IsCalendarEnabled = false); + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => IsCalendarEnabled = false).ConfigureAwait(false); - if (ShouldResetDayRanges(message)) - { - Debug.WriteLine("Will reset day ranges."); - await ClearDayRangeModelsAsync(); - } - else if (ShouldScrollToItem(message)) + if (!IsPageActive(lifetimeVersion)) + return; + + if (!ShouldResetDayRanges(message) && ShouldScrollToItem(message)) { // Scroll to the selected date. - Messenger.Send(new ScrollToDateMessage(message.DisplayDate)); + if (IsPageActive(lifetimeVersion)) + { + Messenger.Send(new ScrollToDateMessage(message.DisplayDate)); + } + Debug.WriteLine("Scrolling to selected date."); return; } @@ -516,10 +736,23 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, // This will replace the whole collection because the user initiated a new render. await RenderDatesAsync(message.CalendarInitInitiative, message.DisplayDate, - CalendarLoadDirection.Replace); + CalendarLoadDirection.Replace, + lifetimeVersion).ConfigureAwait(false); // Scroll to the current hour. - Messenger.Send(new ScrollToHourMessage(TimeSpan.FromHours(DateTime.Now.Hour))); + if (IsPageActive(lifetimeVersion)) + { + Messenger.Send(new ScrollToHourMessage(TimeSpan.FromHours(DateTime.Now.Hour))); + } + } + catch (OperationCanceledException) + { + } + catch (COMException) when (!IsPageActive(lifetimeVersion)) + { + } + catch (ObjectDisposedException) when (!IsPageActive(lifetimeVersion)) + { } catch (Exception ex) { @@ -528,55 +761,88 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, } finally { - _calendarLoadingSemaphore.Release(); + ReleaseCalendarLoadingLock(); - await ExecuteUIThread(() => IsCalendarEnabled = true); + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => IsCalendarEnabled = true).ConfigureAwait(false); } } - private async Task AddDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel) + private async Task AddDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel, long lifetimeVersion) { if (dayRangeRenderModel == null) return; - await ExecuteUIThread(() => + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => { DayRanges.Add(dayRangeRenderModel); }); } - private async Task InsertDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel, int index) + private async Task InsertDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel, int index, long lifetimeVersion) { if (dayRangeRenderModel == null) return; - await ExecuteUIThread(() => + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => { DayRanges.Insert(index, dayRangeRenderModel); }); } - private async Task RemoveDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel) + private async Task RemoveDayRangeModelAsync(DayRangeRenderModel dayRangeRenderModel, long lifetimeVersion) { if (dayRangeRenderModel == null) return; - await ExecuteUIThread(() => + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => { DayRanges.Remove(dayRangeRenderModel); }); } - private async Task ClearDayRangeModelsAsync() + private async Task ClearDayRangeModelsAsync(long lifetimeVersion) { - await ExecuteUIThread(() => + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => { DayRanges.Clear(); }); } + private async Task ReplaceDayRangeModelsAsync(IEnumerable dayRangeRenderModels, DateTime displayDate, long lifetimeVersion) + { + var renderModels = dayRangeRenderModels?.ToList() ?? []; + + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => + { + DayRanges.ReplaceRange(renderModels); + + if (renderModels.Count == 0) + { + SelectedDayRange = null; + SelectedDateRangeIndex = -1; + return; + } + + var selectedIndex = renderModels.FindIndex(model => + displayDate >= model.CalendarRenderOptions.DateRange.StartDate && + displayDate <= model.CalendarRenderOptions.DateRange.EndDate); + + if (selectedIndex < 0) + { + selectedIndex = 0; + } + + SelectedDateRangeIndex = selectedIndex; + SelectedDayRange = renderModels[selectedIndex]; + }); + } + private async Task RenderDatesAsync(CalendarInitInitiative calendarInitInitiative, DateTime? loadingDisplayDate = null, - CalendarLoadDirection calendarLoadDirection = CalendarLoadDirection.Replace) + CalendarLoadDirection calendarLoadDirection = CalendarLoadDirection.Replace, + long lifetimeVersion = 0) { + if (!IsPageActive(lifetimeVersion)) + return; + isLoadMoreBlocked = calendarLoadDirection == CalendarLoadDirection.Replace; // This is the part we arrange the flip view calendar logic. @@ -643,11 +909,17 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, // Dates are loaded. Now load the events for them. foreach (var renderModel in renderModels) { - await InitializeCalendarEventsForDayRangeAsync(renderModel).ConfigureAwait(false); + await InitializeCalendarEventsForDayRangeAsync(renderModel, lifetimeVersion).ConfigureAwait(false); + + if (!IsPageActive(lifetimeVersion)) + return; } // Filter by active calendars. This is a quick operation, and things are not on the UI yet. - FilterActiveCalendars(renderModels); + await FilterActiveCalendarsAsync(renderModels, lifetimeVersion).ConfigureAwait(false); + + if (!IsPageActive(lifetimeVersion)) + return; CalendarLoadDirection animationDirection = calendarLoadDirection; @@ -655,29 +927,26 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, if (calendarLoadDirection == CalendarLoadDirection.Replace) { - // New date ranges are being replaced. - // We must preserve existing selection if any, add the items before/after the current one, remove the current one. - // This will make sure the new dates are animated in the correct direction. - isLoadMoreBlocked = true; + await ReplaceDayRangeModelsAsync(renderModels, displayDate, lifetimeVersion).ConfigureAwait(false); + isLoadMoreBlocked = false; - // Remove all other dates except this one. - var rangesToRemove = DayRanges.Where(a => a != SelectedDayRange).ToList(); - - foreach (var range in rangesToRemove) + if (calendarInitInitiative == CalendarInitInitiative.User) { - await RemoveDayRangeModelAsync(range); + _currentDisplayType = StatePersistanceService.CalendarDisplayType; + _displayDayCount = StatePersistanceService.DayDisplayCount; + + Messenger.Send(new ScrollToDateMessage(displayDate)); } - animationDirection = displayDate <= SelectedDayRange?.CalendarRenderOptions.DateRange.StartDate ? - CalendarLoadDirection.Previous : CalendarLoadDirection.Next; + return; } if (animationDirection == CalendarLoadDirection.Next) { foreach (var item in renderModels) { - await AddDayRangeModelAsync(item); + await AddDayRangeModelAsync(item, lifetimeVersion); } } else if (animationDirection == CalendarLoadDirection.Previous) @@ -690,7 +959,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, // Insert each render model in reverse order. for (int i = renderModels.Count - 1; i >= 0; i--) { - await InsertDayRangeModelAsync(renderModels[i], 0); + await InsertDayRangeModelAsync(renderModels[i], 0, lifetimeVersion); } } @@ -723,12 +992,15 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, } } - private async Task InitializeCalendarEventsForDayRangeAsync(DayRangeRenderModel dayRangeRenderModel) + private async Task InitializeCalendarEventsForDayRangeAsync(DayRangeRenderModel dayRangeRenderModel, long lifetimeVersion) { + if (!IsPageActive(lifetimeVersion)) + return; + // Clear all events first for all days. foreach (var day in dayRangeRenderModel.CalendarDays) { - await ExecuteUIThread(() => + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => { day.EventsCollection.Clear(); }); @@ -739,6 +1011,9 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, foreach (var calendarViewModel in AccountCalendarStateService.AllCalendars) { + if (!IsPageActive(lifetimeVersion)) + return; + // Check all the events for the given date range and calendar. // Then find the day representation for all the events returned, and add to the collection. @@ -746,13 +1021,16 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, foreach (var @event in events) { + if (!IsPageActive(lifetimeVersion)) + return; + // Find the days that the event falls into. var allDaysForEvent = dayRangeRenderModel.CalendarDays.Where(a => a.Period.OverlapsWith(@event.Period)); foreach (var calendarDay in allDaysForEvent) { var calendarItemViewModel = new CalendarItemViewModel(@event); - await ExecuteUIThread(() => + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => { calendarDay.EventsCollection.AddCalendarItem(calendarItemViewModel); }); @@ -763,21 +1041,33 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, private async Task RefreshVisibleRangesAsync() { + var lifetimeVersion = CurrentPageLifetimeVersion; + var hasLoadingLock = false; + try { - await _calendarLoadingSemaphore.WaitAsync().ConfigureAwait(false); + hasLoadingLock = await WaitForCalendarLoadingLockAsync(lifetimeVersion).ConfigureAwait(false); + + if (!hasLoadingLock) + return; if (DayRanges == null || DayRanges.Count == 0) return; RefreshSettings(); - - foreach (var dayRange in DayRanges) - { - await InitializeCalendarEventsForDayRangeAsync(dayRange).ConfigureAwait(false); - } - - FilterActiveCalendars(DayRanges); + await RenderDatesAsync(CalendarInitInitiative.User, + GetRestoreDate(), + CalendarLoadDirection.Replace, + lifetimeVersion).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + catch (COMException) when (!IsPageActive(lifetimeVersion)) + { + } + catch (ObjectDisposedException) when (!IsPageActive(lifetimeVersion)) + { } catch (Exception ex) { @@ -785,7 +1075,10 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, } finally { - _calendarLoadingSemaphore.Release(); + if (hasLoadingLock) + { + ReleaseCalendarLoadingLock(); + } } } @@ -891,9 +1184,15 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, private async Task LoadMoreAsync() { + var lifetimeVersion = CurrentPageLifetimeVersion; + var hasLoadingLock = false; + try { - await _calendarLoadingSemaphore.WaitAsync(); + hasLoadingLock = await WaitForCalendarLoadingLockAsync(lifetimeVersion).ConfigureAwait(false); + + if (!hasLoadingLock) + return; // Depending on the selected index, we'll load more dates. // Day ranges may change while the async update is in progress. @@ -902,20 +1201,33 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, if (SelectedDateRangeIndex == 0) { - await RenderDatesAsync(CalendarInitInitiative.App, calendarLoadDirection: CalendarLoadDirection.Previous); + await RenderDatesAsync(CalendarInitInitiative.App, calendarLoadDirection: CalendarLoadDirection.Previous, lifetimeVersion: lifetimeVersion); } else if (SelectedDateRangeIndex == DayRanges.Count - 1) { - await RenderDatesAsync(CalendarInitInitiative.App, calendarLoadDirection: CalendarLoadDirection.Next); + await RenderDatesAsync(CalendarInitInitiative.App, calendarLoadDirection: CalendarLoadDirection.Next, lifetimeVersion: lifetimeVersion); } } - catch (Exception) + catch (OperationCanceledException) { + } + catch (COMException) when (!IsPageActive(lifetimeVersion)) + { + } + catch (ObjectDisposedException) when (!IsPageActive(lifetimeVersion)) + { + } + catch (Exception ex) + { + Debug.WriteLine(ex); Debugger.Break(); } finally { - _calendarLoadingSemaphore.Release(); + if (hasLoadingLock) + { + ReleaseCalendarLoadingLock(); + } } } @@ -1042,9 +1354,14 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, public async void Receive(AccountRemovedMessage message) { + var lifetimeVersion = CurrentPageLifetimeVersion; + + if (!IsPageActive(lifetimeVersion)) + return; + var removedAccountId = message.Account.Id; - await ExecuteUIThread(() => + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => { foreach (var dayRange in DayRanges) { @@ -1066,6 +1383,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, protected override async void OnCalendarItemDeleted(CalendarItem calendarItem) { base.OnCalendarItemDeleted(calendarItem); + var lifetimeVersion = CurrentPageLifetimeVersion; Debug.WriteLine($"Calendar item deleted: {calendarItem.Id}"); @@ -1080,7 +1398,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, } // Remove the event and its occurrences from all visible date ranges. - await ExecuteUIThread(() => + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => { foreach (var dayRange in DayRanges) { @@ -1097,6 +1415,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, protected override async void OnCalendarItemUpdated(CalendarItem calendarItem, CalendarItemUpdateSource source) { base.OnCalendarItemUpdated(calendarItem, source); + var lifetimeVersion = CurrentPageLifetimeVersion; Debug.WriteLine($"Calendar item updated: {calendarItem.Id}"); // Local-only calendar operations are persisted immediately without real network I/O. @@ -1128,7 +1447,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, .Where(a => a.Period.OverlapsWith(calendarItem.Period)) .ToList(); - await ExecuteUIThread(() => + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => { if (source == CalendarItemUpdateSource.ClientUpdated) { @@ -1178,12 +1497,13 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, } }); - FilterActiveCalendars(DayRanges); + await FilterActiveCalendarsAsync(DayRanges).ConfigureAwait(false); } protected override async void OnCalendarItemAdded(CalendarItem calendarItem) { base.OnCalendarItemAdded(calendarItem); + var lifetimeVersion = CurrentPageLifetimeVersion; Debug.WriteLine($"Calendar item added: {calendarItem.Id}"); // Series master events should not be visible on the UI. @@ -1209,7 +1529,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, { Debug.WriteLine($"Mapped pending busy item {pendingMatch.Id} with synced server event {calendarItem.Id}."); - await ExecuteUIThread(() => + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => { RemoveCalendarItemEverywhere(pendingMatch.Id); }); @@ -1230,17 +1550,19 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, IsBusy = string.IsNullOrEmpty(calendarItem.RemoteEventId) }; - await ExecuteUIThread(() => + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => { calendarDay.EventsCollection.AddCalendarItem(calendarItemViewModel); }); } - FilterActiveCalendars(DayRanges); + await FilterActiveCalendarsAsync(DayRanges).ConfigureAwait(false); } private async Task RestoreVisibleRecurringSeriesInstancesAsync(CalendarItem recurringParent) { + var lifetimeVersion = CurrentPageLifetimeVersion; + if (DayRanges.DisplayRange == null || recurringParent?.AssignedCalendar == null) return; @@ -1254,7 +1576,7 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, if (!recurringChildren.Any()) return; - await ExecuteUIThread(() => + await ExecuteUIThreadIfActiveAsync(lifetimeVersion, () => { foreach (var child in recurringChildren) { @@ -1277,6 +1599,6 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, } }); - FilterActiveCalendars(DayRanges); + await FilterActiveCalendarsAsync(DayRanges).ConfigureAwait(false); } } diff --git a/Wino.Core.Domain/Interfaces/IShellClient.cs b/Wino.Core.Domain/Interfaces/IShellClient.cs index 321d5442..2248a92e 100644 --- a/Wino.Core.Domain/Interfaces/IShellClient.cs +++ b/Wino.Core.Domain/Interfaces/IShellClient.cs @@ -69,5 +69,5 @@ public interface IShellHost { bool HasShellContent { get; } - void ActivateMode(WinoApplicationMode mode, bool isInitialActivation); + void ActivateMode(WinoApplicationMode mode, ShellModeActivationContext activationContext); } diff --git a/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs b/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs index dc6ed40c..7f33d834 100644 --- a/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs +++ b/Wino.Core.ViewModels/SettingOptionsPageViewModel.cs @@ -58,22 +58,14 @@ public partial class SettingOptionsPageViewModel : CoreBaseViewModel }); } - [RelayCommand] - private void GoAccountSettings() => Messenger.Send(); - [RelayCommand] public void NavigateSubDetail(object type) { if (type is WinoPage pageType) { - if (pageType == WinoPage.AccountManagementPage) - { - GoAccountSettings(); - return; - } - string pageTitle = pageType switch { + WinoPage.ManageAccountsPage => Translator.SettingsManageAccountSettings_Title, WinoPage.PersonalizationPage => Translator.SettingsPersonalization_Title, WinoPage.AboutPage => Translator.SettingsAbout_Title, WinoPage.MessageListPage => Translator.SettingsMessageList_Title, diff --git a/Wino.Mail.ViewModels/MailAppShellViewModel.cs b/Wino.Mail.ViewModels/MailAppShellViewModel.cs index 39e534ab..20ea5ad2 100644 --- a/Wino.Mail.ViewModels/MailAppShellViewModel.cs +++ b/Wino.Mail.ViewModels/MailAppShellViewModel.cs @@ -30,7 +30,6 @@ namespace Wino.Mail.ViewModels; public partial class MailAppShellViewModel : MailBaseViewModel, IMailShellClient, - IRecipient, IRecipient, IRecipient, IRecipient, @@ -232,31 +231,19 @@ public partial class MailAppShellViewModel : MailBaseViewModel, base.OnNavigatedTo(mode, parameters); var activationContext = parameters as ShellModeActivationContext; - var isModeResetActivation = activationContext != null; var shouldRunStartupFlows = activationContext?.IsInitialActivation ?? true; + var hasExistingMenuItems = MenuItems?.Any() == true; PreferencesService.PreferenceChanged -= PreferencesServiceChanged; PreferencesService.PreferenceChanged += PreferencesServiceChanged; - if (mode == NavigationMode.Back && !isModeResetActivation) - { - // Preserve current mail/folder selection and active rendering page when - // switching back from Calendar mode. Recreating menu/folder state here - // causes a folder reload, which clears selection and disposes reader page. - // Rehydrate only if menu state is unexpectedly empty. - if (MenuItems?.Any() != true || FooterItems?.Any() != true) - { - await CreateFooterItemsAsync(); - await RecreateMenuItemsAsync(); - await RestoreSelectedAccountAfterMenuRefreshAsync(false); - } - - return; - } - await CreateFooterItemsAsync(true); - await RecreateMenuItemsAsync(); + if (!hasExistingMenuItems) + { + await RecreateMenuItemsAsync(); + } + await ProcessLaunchOptionsAsync(); await ValidateWebView2RuntimeAsync(); @@ -934,8 +921,6 @@ public partial class MailAppShellViewModel : MailBaseViewModel, accountMenuItem.UpdateAccount(accountModel); } - public void Receive(NavigateManageAccountsRequested message) => SelectedMenuItem = ManageAccountsMenuItem; - public async void Receive(MailtoProtocolMessageRequested message) { var accounts = await _accountService.GetAccountsAsync(); @@ -1173,7 +1158,6 @@ public partial class MailAppShellViewModel : MailBaseViewModel, Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); - Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); Messenger.Register(this); @@ -1192,7 +1176,6 @@ public partial class MailAppShellViewModel : MailBaseViewModel, Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); - Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); Messenger.Unregister(this); diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs index 50e56849..cfcd9632 100644 --- a/Wino.Mail.ViewModels/MailListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs @@ -17,8 +17,8 @@ using Wino.Core.Domain.Entities.Mail; using Wino.Core.Domain.Entities.Shared; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models; +using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Menus; using Wino.Core.Domain.Models.Navigation; @@ -198,30 +198,6 @@ public partial class MailListPageViewModel : MailBaseViewModel, SelectedSortingOption = SortingOptions[0]; MailListLength = statePersistenceService.MailListPaneLength; - - //_selectionChangedThrottler = new ThrottledEventHandler(100, () => - //{ - // _ = ExecuteUIThread(() => - // { - // if (MailCollection.SelectedVisibleCount == 1) - // { - // ActiveMailItemChanged(MailCollection.SelectedVisibleItems.ElementAt(0)); - // } - // else - // { - // // At this point, either we don't have any item selected - // // or we have multiple item selected. In either case - // // there should be no active item. - - // ActiveMailItemChanged(null); - // } - - // NotifyItemSelected(); - // SetupTopBarActions(); - // }); - - // ThrottledSelectionChanged?.Invoke(this, EventArgs.Empty); - //}); } public override void OnNavigatedTo(NavigationMode mode, object parameters) diff --git a/Wino.Mail.WinUI/App.xaml.cs b/Wino.Mail.WinUI/App.xaml.cs index f2637826..fa39fb6f 100644 --- a/Wino.Mail.WinUI/App.xaml.cs +++ b/Wino.Mail.WinUI/App.xaml.cs @@ -177,7 +177,7 @@ public partial class App : WinoApplication, services.AddTransient(typeof(SignatureAndEncryptionPageViewModel)); services.AddTransient(typeof(EmailTemplatesPageViewModel)); services.AddTransient(typeof(CreateEmailTemplatePageViewModel)); - services.AddTransient(typeof(CalendarPageViewModel)); + services.AddSingleton(typeof(CalendarPageViewModel)); services.AddTransient(typeof(CalendarSettingsPageViewModel)); services.AddTransient(typeof(CalendarAccountSettingsPageViewModel)); services.AddTransient(typeof(EventDetailsPageViewModel)); diff --git a/Wino.Mail.WinUI/Controls/AppModeFooterSwitcherControl.xaml b/Wino.Mail.WinUI/Controls/AppModeFooterSwitcherControl.xaml index b0cac0d9..1cb5188b 100644 --- a/Wino.Mail.WinUI/Controls/AppModeFooterSwitcherControl.xaml +++ b/Wino.Mail.WinUI/Controls/AppModeFooterSwitcherControl.xaml @@ -2,6 +2,7 @@ x:Class="Wino.Mail.WinUI.Controls.AppModeFooterSwitcherControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals" xmlns:controls="using:CommunityToolkit.WinUI.Controls" xmlns:domain="using:Wino.Core.Domain" Loaded="ControlLoaded" @@ -27,6 +28,11 @@ + + + + + diff --git a/Wino.Mail.WinUI/Controls/AppModeFooterSwitcherControl.xaml.cs b/Wino.Mail.WinUI/Controls/AppModeFooterSwitcherControl.xaml.cs index 42aec0fe..9869d8c6 100644 --- a/Wino.Mail.WinUI/Controls/AppModeFooterSwitcherControl.xaml.cs +++ b/Wino.Mail.WinUI/Controls/AppModeFooterSwitcherControl.xaml.cs @@ -4,6 +4,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain; namespace Wino.Mail.WinUI.Controls; @@ -44,6 +45,13 @@ public sealed partial class AppModeFooterSwitcherControl : UserControl if (_isUpdatingSelection) return; + if (ModeSegmentedControl.SelectedIndex == 3) + { + _navigationService.Navigate(WinoPage.SettingsPage); + UpdateSelection(_statePersistenceService.ApplicationMode); + return; + } + var selectedMode = ModeSegmentedControl.SelectedIndex switch { 1 => WinoApplicationMode.Calendar, diff --git a/Wino.Mail.WinUI/Controls/Calendar/CustomCalendarFlipView.cs b/Wino.Mail.WinUI/Controls/Calendar/CustomCalendarFlipView.cs index 55b94128..4e3a812c 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/CustomCalendarFlipView.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/CustomCalendarFlipView.cs @@ -49,6 +49,7 @@ public partial class CustomCalendarFlipView : FlipView HideButton(PreviousButtonVertical); HideButton(NextButtonVertical); + SelectionChanged -= FlipViewSelectionChanged; SelectionChanged += FlipViewSelectionChanged; } diff --git a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarControl.cs b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarControl.cs index ab33d8fb..b32a9d17 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarControl.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarControl.cs @@ -13,7 +13,7 @@ using Wino.Helpers; namespace Wino.Calendar.Controls; -public partial class WinoCalendarControl : Control +public partial class WinoCalendarControl : Control, IDisposable { private const string PART_WinoFlipView = nameof(PART_WinoFlipView); private const string PART_IdleGrid = nameof(PART_IdleGrid); @@ -93,6 +93,12 @@ public partial class WinoCalendarControl : Control partial void OnIsFlipIdleChanged(bool newValue) => UpdateIdleState(); + partial void OnDayRangesChanged(ObservableCollection? newValue) + => EnsureStableSelection(); + + partial void OnSelectedFlipViewDayRangeChanged(DayRangeRenderModel? newValue) + => EnsureStableSelection(); + partial void OnActiveScrollViewerPropertyChanged(DependencyPropertyChangedEventArgs e) { var newValue = e.NewValue as ScrollViewer; @@ -200,6 +206,23 @@ public partial class WinoCalendarControl : Control if (InternalFlipView != null) { InternalFlipView.ProgrammaticNavigationCompleted -= InternalFlipViewProgrammaticNavigationCompleted; + + if (InternalFlipView is IDisposable disposableFlipView) + { + disposableFlipView.Dispose(); + } + } + + if (_previousScrollViewer != null) + { + DeregisterScrollChanges(_previousScrollViewer); + _previousScrollViewer = null; + } + + if (_previousCanvas != null) + { + DeregisterCanvas(_previousCanvas); + _previousCanvas = null; } InternalFlipView = GetTemplateChild(PART_WinoFlipView) as WinoCalendarFlipView; @@ -213,6 +236,7 @@ public partial class WinoCalendarControl : Control UpdateIdleState(); ManageCalendarOrientation(); ManageDisplayType(); + EnsureStableSelection(); } private void InternalFlipViewProgrammaticNavigationCompleted(object? sender, ProgrammaticNavigationCompletedEventArgs e) @@ -233,6 +257,33 @@ public partial class WinoCalendarControl : Control } } + private void EnsureStableSelection() + { + if (InternalFlipView == null || DayRanges == null || DayRanges.Count == 0) + return; + + var targetIndex = SelectedFlipViewIndex; + + if (SelectedFlipViewDayRange != null) + { + var selectedRangeIndex = DayRanges.IndexOf(SelectedFlipViewDayRange); + if (selectedRangeIndex >= 0) + { + targetIndex = selectedRangeIndex; + } + } + + if (targetIndex < 0 || targetIndex >= DayRanges.Count) + { + targetIndex = 0; + } + + if (InternalFlipView.SelectedIndex != targetIndex) + { + InternalFlipView.SelectedIndex = targetIndex; + } + } + private void ActiveTimelineCellUnselected(object? sender, TimelineCellUnselectedArgs e) => TimelineCellUnselected?.Invoke(this, e); @@ -291,4 +342,40 @@ public partial class WinoCalendarControl : Control { return this.FindDescendants().FirstOrDefault(a => a.CalendarItem == calendarItemViewModel)!; } + + public void Dispose() + { + SizeChanged -= CalendarSizeChanged; + + if (_previousScrollViewer != null) + { + DeregisterScrollChanges(_previousScrollViewer); + _previousScrollViewer = null; + } + + if (_previousCanvas != null) + { + DeregisterCanvas(_previousCanvas); + _previousCanvas = null; + } + + if (InternalFlipView != null) + { + InternalFlipView.ProgrammaticNavigationCompleted -= InternalFlipViewProgrammaticNavigationCompleted; + + if (InternalFlipView is IDisposable disposableFlipView) + { + disposableFlipView.Dispose(); + } + + InternalFlipView = null; + } + + IdleGrid = null; + ActiveCanvas = null; + ActiveScrollViewer = null; + TimelineCellSelected = null; + TimelineCellUnselected = null; + ScrollPositionChanging = null; + } } diff --git a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarFlipView.cs b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarFlipView.cs index 7aa9b2c5..c1bdadaf 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarFlipView.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarFlipView.cs @@ -10,7 +10,7 @@ using Wino.Core.Domain.Models.Calendar; namespace Wino.Calendar.Controls; -public partial class WinoCalendarFlipView : CustomCalendarFlipView +public partial class WinoCalendarFlipView : CustomCalendarFlipView, IDisposable { public static readonly DependencyProperty IsIdleProperty = DependencyProperty.Register(nameof(IsIdle), typeof(bool), typeof(WinoCalendarFlipView), new PropertyMetadata(true)); public static readonly DependencyProperty ActiveCanvasProperty = DependencyProperty.Register(nameof(ActiveCanvas), typeof(WinoDayTimelineCanvas), typeof(WinoCalendarFlipView), new PropertyMetadata(null)); @@ -50,10 +50,11 @@ public partial class WinoCalendarFlipView : CustomCalendarFlipView internal event EventHandler? ProgrammaticNavigationCompleted; private INotifyCollectionChanged? _trackedItemsSource; + private readonly long _itemsSourceCallbackToken; public WinoCalendarFlipView() { - RegisterPropertyChangedCallback(ItemsSourceProperty, new DependencyPropertyChangedCallback(OnItemsSourceChanged)); + _itemsSourceCallbackToken = RegisterPropertyChangedCallback(ItemsSourceProperty, new DependencyPropertyChangedCallback(OnItemsSourceChanged)); } private static void OnItemsSourceChanged(DependencyObject d, DependencyProperty e) @@ -207,6 +208,18 @@ public partial class WinoCalendarFlipView : CustomCalendarFlipView private ObservableRangeCollection? GetItemsSource() => ItemsSource as ObservableRangeCollection; + + public void Dispose() + { + if (_trackedItemsSource != null) + { + _trackedItemsSource.CollectionChanged -= ItemsSourceUpdated; + _trackedItemsSource = null; + } + + UnregisterPropertyChangedCallback(ItemsSourceProperty, _itemsSourceCallbackToken); + ProgrammaticNavigationCompleted = null; + } } internal sealed class ProgrammaticNavigationCompletedEventArgs : EventArgs diff --git a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarView.cs b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarView.cs index 7ff9dd51..e00305d3 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarView.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarView.cs @@ -10,7 +10,7 @@ using Wino.Helpers; namespace Wino.Calendar.Controls; -public partial class WinoCalendarView : Control +public partial class WinoCalendarView : Control, IDisposable { private const string PART_DayViewItemBorder = nameof(PART_DayViewItemBorder); private const string PART_CalendarView = nameof(PART_CalendarView); @@ -54,6 +54,7 @@ public partial class WinoCalendarView : Control private CalendarView? CalendarView; + private long _displayModeCallbackToken = -1; public WinoCalendarView() { @@ -64,6 +65,17 @@ public partial class WinoCalendarView : Control { base.OnApplyTemplate(); + if (CalendarView != null) + { + CalendarView.SelectedDatesChanged -= InternalCalendarViewSelectionChanged; + + if (_displayModeCallbackToken != -1) + { + CalendarView.UnregisterPropertyChangedCallback(CalendarView.DisplayModeProperty, _displayModeCallbackToken); + _displayModeCallbackToken = -1; + } + } + CalendarView = GetTemplateChild(PART_CalendarView) as CalendarView; Guard.IsNotNull(CalendarView, nameof(CalendarView)); @@ -78,7 +90,7 @@ public partial class WinoCalendarView : Control // Everytime display mode changes, update the visible date range backgrounds. // If users go back from year -> month -> day, we need to update the visible date range backgrounds. - CalendarView.RegisterPropertyChangedCallback(CalendarView.DisplayModeProperty, (s, e) => UpdateVisibleDateRangeBackgrounds()); + _displayModeCallbackToken = CalendarView.RegisterPropertyChangedCallback(CalendarView.DisplayModeProperty, (s, e) => UpdateVisibleDateRangeBackgrounds()); } private void InternalCalendarViewSelectionChanged(CalendarView sender, CalendarViewSelectedDatesChangedEventArgs args) @@ -147,4 +159,20 @@ public partial class WinoCalendarView : Control } } } + + public void Dispose() + { + if (CalendarView == null) + return; + + CalendarView.SelectedDatesChanged -= InternalCalendarViewSelectionChanged; + + if (_displayModeCallbackToken != -1) + { + CalendarView.UnregisterPropertyChangedCallback(CalendarView.DisplayModeProperty, _displayModeCallbackToken); + _displayModeCallbackToken = -1; + } + + CalendarView = null; + } } diff --git a/Wino.Mail.WinUI/Properties/launchSettings.json b/Wino.Mail.WinUI/Properties/launchSettings.json index 14643837..52fff1db 100644 --- a/Wino.Mail.WinUI/Properties/launchSettings.json +++ b/Wino.Mail.WinUI/Properties/launchSettings.json @@ -3,7 +3,7 @@ "Wino.Mail.WinUI (Package)": { "commandName": "MsixPackage", "doNotLaunchApp": false, - "nativeDebugging": true + "nativeDebugging": false }, "Wino.Mail.WinUI (Unpackaged)": { "commandName": "Project" diff --git a/Wino.Mail.WinUI/Services/NavigationService.cs b/Wino.Mail.WinUI/Services/NavigationService.cs index 07e43fdf..1a1fde0d 100644 --- a/Wino.Mail.WinUI/Services/NavigationService.cs +++ b/Wino.Mail.WinUI/Services/NavigationService.cs @@ -215,7 +215,10 @@ public class NavigationService : NavigationServiceBase, INavigationService if (coreFrame.Content is IShellHost shell) { - shell.ActivateMode(mode, isInitialShellNavigation); + shell.ActivateMode(mode, new ShellModeActivationContext + { + IsInitialActivation = isInitialShellNavigation + }); return true; } diff --git a/Wino.Mail.WinUI/ShellWindow.xaml b/Wino.Mail.WinUI/ShellWindow.xaml index 99c29e40..1276d551 100644 --- a/Wino.Mail.WinUI/ShellWindow.xaml +++ b/Wino.Mail.WinUI/ShellWindow.xaml @@ -93,7 +93,7 @@ + xmlns:primitives="using:Microsoft.UI.Xaml.Controls.Primitives" + xmlns:winoControls="using:Wino.Mail.WinUI.Controls">