Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/Controls/src/Core/Shell/Shell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,27 @@ internal static BackButtonBehavior GetEffectiveBackButtonBehavior(BindableObject
/// <param name="value">The View to be displayed in the navigation bar.</param>
public static void SetTitleView(BindableObject obj, View value) => obj.SetValue(TitleViewProperty, value);

// Determines whether the Shell's Title was set by the user (explicit code, style, or a binding)
// as opposed to being mirrored from the current page by the renderer (FromHandler) or never set
// at all (DefaultValue). This lets ShellToolbar mirror the page title into Shell.Title for
// TitleView bindings without clobbering a title the user set intentionally.
internal bool IsTitleSetByUser()
{
if (GetIsBound(TitleProperty))
return true;

var context = GetContext(TitleProperty);
if (context is null)
return false;

var specificity = context.Values.GetSpecificity();
return specificity != SetterSpecificity.DefaultValue && specificity != SetterSpecificity.FromHandler;
}

// Returns the Title only when it was set by the user. Used by the native window title
// fallback so that a renderer-mirrored page title never leaks into the platform chrome.
internal string GetUserSetTitle() => IsTitleSetByUser() ? Title : null;

static void OnFlyoutBehaviorChanged(BindableObject bindable, object oldValue, object newValue)
{
var element = (Element)bindable;
Expand Down
33 changes: 11 additions & 22 deletions src/Controls/src/Core/ShellToolbar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -170,16 +170,18 @@ internal void UpdateTitle()
Shell.GetTitleView(_shell));

var title = GetCurrentTitle();
if (!IsShellTitleSetByUser())
_shell.SetValueFromRenderer(Shell.TitleProperty, title);

if (TitleView != null)
{
Title = String.Empty;
return;
}

Title = title;
// Mirror the current page's title into Shell.Title so that bindings inside a custom
// TitleView (e.g. {Binding Title, Source={x:Reference shell}}) can resolve to it.
// Use SetValueFromRenderer (FromHandler specificity) so the value is recognized as
// renderer-generated and does not leak into the native window title (see Window.cs),
// and only when the user hasn't explicitly set Shell.Title themselves.
if (!_shell.IsTitleSetByUser())
_shell.SetValueFromRenderer(Page.TitleProperty, title);

// The native nav-bar title must be empty when a custom TitleView is present,
// otherwise it should reflect the current page title.
Title = TitleView != null ? String.Empty : title;
}

string GetCurrentTitle()
Expand All @@ -198,18 +200,5 @@ string GetCurrentTitle()

return String.Empty;
}

bool IsShellTitleSetByUser()
{
var titleContext = _shell.GetContext(Shell.TitleProperty);
if (titleContext == null)
return false;

if (titleContext.Bindings.Count > 0)
return true;

var specificity = titleContext.Values.GetSpecificity();
return specificity != SetterSpecificity.DefaultValue && specificity != SetterSpecificity.FromHandler;
}
}
}
2 changes: 1 addition & 1 deletion src/Controls/src/Core/Window/Window.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public bool IsActivated
private set => SetValue(IsActivatedPropertyKey, value);
}

string? ITitledElement.Title => Title ?? (Page as Shell)?.Title;
string? ITitledElement.Title => Title ?? (Page as Shell)?.GetUserSetTitle();

public Page? Page
{
Expand Down
31 changes: 31 additions & 0 deletions src/Controls/tests/Core.UnitTests/ShellToolbarTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,37 @@ public void ShellTitleReflectsCurrentPageTitleForTitleViewBindings()
Assert.Equal("Updated Test Title", label.Text);
}

// Regression test for https://github.com/dotnet/maui/issues/36562
// PR #35800 made ShellToolbar mirror the current page's title into Shell.Title (via
// SetValueFromRenderer) so TitleView bindings like {Binding Title, Source={x:Reference Shell}}
// resolve correctly. That mirrored value leaked into the native window title
// (ITitledElement.Title), which previously stayed empty in this scenario, breaking Shell UI
// tests around title/flyout layout on macOS/Windows.
[Fact]
public void WindowNativeTitleDoesNotLeakToolbarMirroredPageTitle()
{
var contentPage = new ContentPage() { Title = "Test Title" };
var titleView = new VerticalStackLayout();

TestShell testShell = new TestShell(contentPage);
var window = new Window()
{
Page = testShell
};

Shell.SetTitleView(contentPage, titleView);

// Shell.Title is mirrored from the page for TitleView binding purposes...
Assert.Equal("Test Title", testShell.Title);

// ...but that mirrored value must NOT leak into the native window title fallback.
Assert.Null(((ITitledElement)window).Title);

contentPage.Title = "Updated Test Title";
Assert.Equal("Updated Test Title", testShell.Title);
Assert.Null(((ITitledElement)window).Title);
}

[Fact]
public void ShellTitleBindingIsNotOverwrittenByCurrentPageTitle()
{
Expand Down
Loading