Back navigation and shell improvements.

This commit is contained in:
Burak Kaan Köse
2026-03-24 18:05:09 +01:00
parent d699818c6f
commit 27c90d2f89
10 changed files with 96 additions and 86 deletions
@@ -28,11 +28,6 @@ public interface IStatePersistanceService : INotifyPropertyChanged
/// </summary>
bool IsReaderNarrowed { get; set; }
/// <summary>
/// Should display back button on the shell title bar.
/// </summary>
bool IsBackButtonVisible { get; }
/// <summary>
/// Current application mode (Mail or Calendar).
/// Not persisted to configuration, only kept in memory.
@@ -44,11 +39,6 @@ public interface IStatePersistanceService : INotifyPropertyChanged
/// </summary>
bool IsEventDetailsVisible { get; set; }
/// <summary>
/// Whether the current application mode has an active backstack that can be navigated.
/// </summary>
bool HasCurrentModeBackStack { get; set; }
/// <summary>
/// Setting: Opened pane length for the navigation view.
/// </summary>
@@ -13,5 +13,6 @@ public interface INavigationService
Type GetPageType(WinoPage winoPage);
bool ChangeApplicationMode(WinoApplicationMode mode);
bool CanGoBack();
void GoBack(NavigationTransitionEffect slideEffect = NavigationTransitionEffect.FromRight);
}
@@ -20,12 +20,20 @@ public sealed class CalendarRangeTextFormatter : ICalendarRangeTextFormatter
return FormatDate(range.StartDate, culture);
}
if (range.SpansSingleMonth)
{
return $"{FormatDate(range.StartDate, culture)} - {FormatDay(range.EndDate, culture)}";
}
return $"{FormatDate(range.StartDate, culture)} - {FormatDate(range.EndDate, culture)}";
}
private static string FormatDate(DateOnly date, CultureInfo culture)
=> date.ToString(culture.DateTimeFormat.MonthDayPattern, culture);
private static string FormatDay(DateOnly date, CultureInfo culture)
=> date.Day.ToString(culture);
private static string FormatMonth(DateOnly date, CultureInfo culture)
=> date.ToString(culture.DateTimeFormat.YearMonthPattern, culture);
}
@@ -32,7 +32,7 @@ public class CalendarRangeTextFormatterTests
startDate: new DateOnly(2026, 3, 3),
endDate: new DateOnly(2026, 3, 10));
Formatter.Format(range, DateContextProvider).Should().Be("March 3 - March 10");
Formatter.Format(range, DateContextProvider).Should().Be("March 3 - 10");
}
[Fact]
+36 -27
View File
@@ -219,6 +219,9 @@ public class NavigationService : NavigationServiceBase, INavigationService
public bool ChangeApplicationMode(WinoApplicationMode mode)
=> ExecuteOnNavigationThread(() => ChangeApplicationModeInternal(mode));
public bool CanGoBack()
=> ExecuteOnNavigationThread(CanGoBackInternal);
private bool ChangeApplicationModeInternal(WinoApplicationMode mode, object? activationParameter = null)
{
var coreFrame = GetCoreFrameInternal(NavigationReferenceFrame.ShellFrame);
@@ -333,7 +336,6 @@ public class NavigationService : NavigationServiceBase, INavigationService
if (innerShellFrame.CanGoBack && lastBackStackEntry?.SourcePageType == pageType)
{
innerShellFrame.GoBack();
UpdateCurrentModeBackStackState(innerShellFrame);
WeakReferenceMessenger.Default.Send(loadCalendarMessage);
return true;
}
@@ -495,7 +497,7 @@ public class NavigationService : NavigationServiceBase, INavigationService
if (navigationResult)
{
UpdateCurrentModeBackStackState(frame);
return true;
}
return navigationResult;
@@ -522,15 +524,13 @@ public class NavigationService : NavigationServiceBase, INavigationService
var currentApplicationMode = _statePersistanceService.ApplicationMode;
if (currentApplicationMode == WinoApplicationMode.Settings &&
_statePersistanceService.HasCurrentModeBackStack)
innerShellFrame?.Content is SettingsPage settingsPage)
{
WeakReferenceMessenger.Default.Send(new BackBreadcrumNavigationRequested(slideEffect));
return;
}
if (settingsPage.CanNavigateBack)
{
WeakReferenceMessenger.Default.Send(new BackBreadcrumNavigationRequested(slideEffect));
}
if (currentApplicationMode == WinoApplicationMode.Settings &&
innerShellFrame?.Content is SettingsPage)
{
return;
}
@@ -544,7 +544,6 @@ public class NavigationService : NavigationServiceBase, INavigationService
if (innerShellFrame?.CanGoBack == true)
{
innerShellFrame.GoBack();
UpdateCurrentModeBackStackState(innerShellFrame);
}
else if (innerShellFrame != null && innerShellFrame.Content?.GetType() != typeof(CalendarPage))
{
@@ -560,16 +559,11 @@ public class NavigationService : NavigationServiceBase, INavigationService
{
// Mail mode: Clear selections and dispose rendering frame
_statePersistanceService.IsReadingMail = false;
_statePersistanceService.HasCurrentModeBackStack = false;
WeakReferenceMessenger.Default.Send(new ClearMailSelectionsRequested());
WeakReferenceMessenger.Default.Send(new DisposeRenderingFrameRequested());
}
}
else
{
UpdateCurrentModeBackStackState(innerShellFrame);
}
}
private void ResetCurrentModeBackStackState()
@@ -581,17 +575,6 @@ public class NavigationService : NavigationServiceBase, INavigationService
innerShellFrame.BackStack.Clear();
innerShellFrame.ForwardStack.Clear();
}
_statePersistanceService.HasCurrentModeBackStack = false;
}
private void UpdateCurrentModeBackStackState(Frame? innerShellFrame)
{
if (_statePersistanceService.ApplicationMode == WinoApplicationMode.Settings)
return;
_statePersistanceService.HasCurrentModeBackStack = _statePersistanceService.ApplicationMode == WinoApplicationMode.Calendar &&
innerShellFrame?.CanGoBack == true;
}
private void PruneInnerShellBackStackForMode(Frame frame, WinoApplicationMode mode)
@@ -636,7 +619,33 @@ public class NavigationService : NavigationServiceBase, INavigationService
frame.BackStack.Clear();
frame.ForwardStack.Clear();
UpdateCurrentModeBackStackState(frame);
}
private bool CanGoBackInternal()
{
var innerShellFrame = GetCoreFrameInternal(NavigationReferenceFrame.InnerShellFrame);
return _statePersistanceService.ApplicationMode switch
{
WinoApplicationMode.Mail => _statePersistanceService.IsReadingMail && _statePersistanceService.IsReaderNarrowed,
WinoApplicationMode.Settings => innerShellFrame?.Content is SettingsPage settingsPage && settingsPage.CanNavigateBack,
WinoApplicationMode.Calendar or WinoApplicationMode.Contacts => HasModeScopedBackStack(innerShellFrame, _statePersistanceService.ApplicationMode),
_ => false
};
}
private bool HasModeScopedBackStack(Frame? innerShellFrame, WinoApplicationMode mode)
{
if (innerShellFrame == null || innerShellFrame.BackStack.Count == 0)
return false;
for (int i = innerShellFrame.BackStack.Count - 1; i >= 0; i--)
{
if (IsPageTypeAllowedInMode(mode, innerShellFrame.BackStack[i].SourcePageType))
return true;
}
return false;
}
// Standalone EML viewer.
@@ -32,23 +32,12 @@ public class StatePersistenceService : ObservableObject, IStatePersistanceServic
private void ServicePropertyChanged(object? sender, PropertyChangedEventArgs e) => StatePropertyChanged?.Invoke(this, e?.PropertyName ?? string.Empty);
public bool IsBackButtonVisible =>
ApplicationMode == WinoApplicationMode.Mail
? (IsReadingMail && IsReaderNarrowed) || HasCurrentModeBackStack
: HasCurrentModeBackStack;
private WinoApplicationMode applicationMode = WinoApplicationMode.Mail;
public WinoApplicationMode ApplicationMode
{
get => applicationMode;
set
{
if (SetProperty(ref applicationMode, value))
{
OnPropertyChanged(nameof(IsBackButtonVisible));
}
}
set => SetProperty(ref applicationMode, value);
}
private bool isEventDetailsVisible;
@@ -60,40 +49,18 @@ public class StatePersistenceService : ObservableObject, IStatePersistanceServic
{
if (SetProperty(ref isEventDetailsVisible, value))
{
OnPropertyChanged(nameof(IsBackButtonVisible));
IsReaderNarrowed = value;
IsReadingMail = value;
}
}
}
private bool hasCurrentModeBackStack;
public bool HasCurrentModeBackStack
{
get => hasCurrentModeBackStack;
set
{
if (SetProperty(ref hasCurrentModeBackStack, value))
{
OnPropertyChanged(nameof(IsBackButtonVisible));
}
}
}
private bool isReadingMail;
public bool IsReadingMail
{
get => isReadingMail;
set
{
if (SetProperty(ref isReadingMail, value))
{
OnPropertyChanged(nameof(IsBackButtonVisible));
}
}
set => SetProperty(ref isReadingMail, value);
}
private bool shouldShiftMailRenderingDesign;
@@ -109,13 +76,7 @@ public class StatePersistenceService : ObservableObject, IStatePersistanceServic
public bool IsReaderNarrowed
{
get => isReaderNarrowed;
set
{
if (SetProperty(ref isReaderNarrowed, value))
{
OnPropertyChanged(nameof(IsBackButtonVisible));
}
}
set => SetProperty(ref isReaderNarrowed, value);
}
private string coreWindowTitle = string.Empty;
+4 -1
View File
@@ -31,10 +31,13 @@
VerticalContentAlignment="Stretch"
BackRequested="BackButtonClicked"
Background="Transparent"
IsBackButtonVisible="{x:Bind StatePersistanceService.IsBackButtonVisible, Mode=OneWay}"
IsPaneToggleButtonVisible="True"
PaneToggleRequested="PaneButtonClicked"
Subtitle="{x:Bind StatePersistanceService.CoreWindowTitle, Mode=OneWay}">
<TitleBar.Resources>
<HorizontalAlignment x:Key="TitleBarContentHorizontalAlignment">Stretch</HorizontalAlignment>
<VerticalAlignment x:Key="TitleBarContentVerticalAlignment">Stretch</VerticalAlignment>
</TitleBar.Resources>
<TitleBar.RightHeader>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button
+40
View File
@@ -46,12 +46,14 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
private ICalendarShellClient? _activeCalendarClient;
private readonly CalendarTitleBarContent _calendarTitleBarContent = new();
private long _calendarTypeSelectorChangedToken;
private bool _isBackButtonVisibilityReady;
public ShellWindow()
{
RegisterRecipients();
InitializeComponent();
StatePersistanceService.StatePropertyChanged += StatePersistenceServiceChanged;
MinWidth = 420;
MinHeight = 420;
@@ -138,7 +140,9 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
_ = StartCalendarReminderServerAsync();
}
_isBackButtonVisibilityReady = true;
ApplyTitleBarContent();
RefreshBackButtonVisibility();
}
private async Task StartCalendarReminderServerAsync()
@@ -166,6 +170,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
public void Receive(TitleBarShellContentUpdated message)
{
ApplyTitleBarContent();
RefreshBackButtonVisibility();
}
public void Receive(ApplicationThemeChanged message)
@@ -279,6 +284,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
{
AttachCalendarClient(null);
ShellTitleBar.Content = MainShellFrame.Content is BasePage basePage ? basePage.ShellContent : null;
RefreshBackButtonVisibility();
return;
}
@@ -288,10 +294,43 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
{
RefreshCalendarSelector();
ShellTitleBar.Content = _calendarTitleBarContent;
RefreshBackButtonVisibility();
return;
}
ShellTitleBar.Content = shellPage.GetShellFrame().Content is BasePage page ? page.ShellContent : null;
RefreshBackButtonVisibility();
}
private void StatePersistenceServiceChanged(object? sender, string propertyName)
{
if (!DispatcherQueue.HasThreadAccess)
{
var enqueued = DispatcherQueue.TryEnqueue(() => StatePersistenceServiceChanged(sender, propertyName));
if (!enqueued)
throw new InvalidOperationException("Could not marshal shell state changes onto the UI thread.");
return;
}
if (propertyName == nameof(IStatePersistanceService.ApplicationMode) ||
propertyName == nameof(IStatePersistanceService.IsReadingMail) ||
propertyName == nameof(IStatePersistanceService.IsReaderNarrowed) ||
propertyName == nameof(IStatePersistanceService.IsEventDetailsVisible))
{
RefreshBackButtonVisibility();
}
}
private void RefreshBackButtonVisibility()
{
if (!_isBackButtonVisibilityReady)
{
ShellTitleBar.IsBackButtonVisible = false;
return;
}
ShellTitleBar.IsBackButtonVisible = NavigationService.CanGoBack();
}
private void AttachCalendarClient(ICalendarShellClient? calendarClient)
@@ -390,6 +429,7 @@ public sealed partial class ShellWindow : WindowEx, IWinoShellWindow,
{
AppWindow.Closing -= OnAppWindowClosing;
AttachCalendarClient(null);
StatePersistanceService.StatePropertyChanged -= StatePersistenceServiceChanged;
_calendarTitleBarContent.UnregisterSelectedTypeChanged(_calendarTypeSelectorChangedToken);
_calendarTitleBarContent.PreviousDateRequested -= CalendarTitleBarContentPreviousDateRequested;
_calendarTitleBarContent.NextDateRequested -= CalendarTitleBarContentNextDateRequested;
+3 -4
View File
@@ -77,9 +77,6 @@ public sealed partial class SettingsPage : SettingsPageAbstract,
// Unregister frame navigation event
SettingsFrame.Navigated -= SettingsFrameNavigated;
// Reset navigation state when leaving SettingsPage
ViewModel.StatePersistenceService.HasCurrentModeBackStack = false;
base.OnNavigatingFrom(e);
}
@@ -225,9 +222,11 @@ public sealed partial class SettingsPage : SettingsPageAbstract,
private void UpdateBackNavigationState()
{
ViewModel.StatePersistenceService.HasCurrentModeBackStack = PageHistory.Count > 1 && SettingsFrame.CanGoBack;
WeakReferenceMessenger.Default.Send(new TitleBarShellContentUpdated());
}
public bool CanNavigateBack => PageHistory.Count > 1 && SettingsFrame.CanGoBack;
private async Task RefreshCurrentPageStateAsync()
{
var activePage = PageHistory.LastOrDefault()?.Request.PageType ?? WinoPage.SettingOptionsPage;
@@ -157,7 +157,6 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
private void ResetShellModeNavigationState()
{
ViewModel.StatePersistenceService.HasCurrentModeBackStack = false;
InnerShellFrame.BackStack.Clear();
InnerShellFrame.ForwardStack.Clear();
}