-
Notifications
You must be signed in to change notification settings - Fork 784
Description
Describe the bug
After performing any CRUD operation that modifies NavigationView.MenuItems collection (using Clear() followed by Add()), any subsequent attempt to change the IsPaneOpen property throws a COM exception with HResult 0x80004005. The NavigationView enters a permanently corrupted state
where the internal SplitView binding appears broken, making the pane toggle completely unusable until the next MenuItems refresh.
Exception Details:
System.Runtime.InteropServices.COMException (0x80004005)
Message: Unspecified error / Erreur non spécifiée
HResult: 0x80004005 (-2147467259)
Stack Trace:
at WinRT.ExceptionHelpers.g__Throw|38_0(Int32 hr)
at ABI.Microsoft.UI.Xaml.Controls.INavigationViewMethods.set_IsPaneOpen(IObjectReference _obj, Boolean value)
at Microsoft.UI.Xaml.Controls.NavigationView.set_IsPaneOpen(Boolean value)
This occurs consistently in:
- Debug builds
- Release builds
- Published/deployed applications
- Multiple Windows 11 machines
Why is this important?
This bug makes NavigationView completely unusable in any application that requires dynamic profile/item management. In my use case:
- User workflow: Users manage game profiles with add/rename/delete operations, which require refreshing the NavigationView items
- Blocked functionality: After any profile CRUD operation, users cannot toggle the navigation pane open/closed
- User experience impact: The pane toggle button becomes non-functional, forcing users to either:
- Restart the application to regain pane control
- Perform another CRUD operation hoping it temporarily fixes the state
- Live with a permanently expanded or collapsed pane
This effectively breaks the core navigation UX pattern recommended by Microsoft for WinUI 3 applications. Any app using NavigationView with dynamic content faces this issue.
Real-world scenario:
- User adds a new game profile → NavigationView refreshes → User tries to collapse pane for more screen space → App appears frozen/broken because toggle silently fails
- This happens in production, not just development, making it a release-blocking issue
Steps to reproduce the bug
Minimal Reproduction test:
MainWindow.xaml:
<Window x:Class="NavigationViewBugRepro.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="NavigationView COM Exception Repro">
<Grid Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Padding="16" Spacing="8">
<TextBlock Text="NavigationView COMException repro (MenuItems.Clear + Add)" FontSize="18" FontWeight="SemiBold" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Button x:Name="TogglePaneButton" Content="Toggle Pane" Click="TogglePaneButton_Click" />
<Button x:Name="ModifyItemsButton" Content="Refresh MenuItems (Clear + Add)" Click="ModifyItemsButton_Click" />
<Button x:Name="AddProfileButton" Content="Add Profile" Click="AddProfileButton_Click" />
<Button x:Name="AddSubProfileButton" Content="Add Sub-Profile to Selected" Click="AddSubProfileButton_Click" />
</StackPanel>
<TextBlock x:Name="StatusText" Text="Status: Ready" />
</StackPanel>
<NavigationView x:Name="MainNavigationView"
Grid.Row="1"
PaneDisplayMode="Left"
OpenPaneLength="250"
CompactPaneLength="60"
CompactModeThresholdWidth="0"
ExpandedModeThresholdWidth="1000"
IsBackButtonVisible="Collapsed"
IsPaneToggleButtonVisible="False"
IsSettingsVisible="True"
PaneOpened="MainNavigationView_PaneOpened"
PaneClosed="MainNavigationView_PaneClosed"
SelectionChanged="MainNavigationView_SelectionChanged">
<NavigationView.PaneHeader>
<TextBlock Text="Profiles" Margin="16,10,0,10" />
</NavigationView.PaneHeader>
<NavigationView.Content>
<StackPanel Padding="20" Spacing="10">
<TextBlock Text="Repro steps:" FontWeight="SemiBold" />
<TextBlock Text="1. Toggle pane (works)." />
<TextBlock Text="2. Click Refresh MenuItems (Clear + Add)." />
<TextBlock Text="3. Toggle pane again -> COMException." />
<TextBlock Text="Profiles have sub-profiles and initials in compact mode." />
</StackPanel>
</NavigationView.Content>
</NavigationView>
</Grid>
</Window>MainWindow.xaml.cs (with models):
public class SubProfile { public Guid Id { get; set; } = Guid.NewGuid(); public string Name { get; set; } = string.Empty; public string? HotKey { get; set; } }
public class GameProfile { public Guid Id { get; set; } = Guid.NewGuid(); public string Name { get; set; } = string.Empty; public string? HotKey { get; set; } public ObservableCollection<SubProfile> SubProfiles { get; } = new(); }
public sealed partial class MainWindow : Window
{
private readonly ObservableCollection<GameProfile> _profiles = new();
private GameProfile? _selectedProfile;
public MainWindow()
{
InitializeComponent();
SeedInitialProfiles();
BuildNavigationItems();
}
private void SeedInitialProfiles()
{
_profiles.Clear();
var shooter = new GameProfile { Name = "Shooter Alpha", HotKey = "Ctrl+1" };
shooter.SubProfiles.Add(new SubProfile { Name = "Default", HotKey = "F1" });
shooter.SubProfiles.Add(new SubProfile { Name = "Sniper", HotKey = "F2" });
var racer = new GameProfile { Name = "Racer Bravo", HotKey = "Ctrl+2" };
racer.SubProfiles.Add(new SubProfile { Name = "Street", HotKey = "F3" });
racer.SubProfiles.Add(new SubProfile { Name = "Track", HotKey = "F4" });
var mmo = new GameProfile { Name = "MMO Charlie", HotKey = "Ctrl+3" };
mmo.SubProfiles.Add(new SubProfile { Name = "Raid", HotKey = "F5" });
_profiles.Add(shooter); _profiles.Add(racer); _profiles.Add(mmo); _selectedProfile = _profiles.FirstOrDefault();
}
private void BuildNavigationItems()
{
var expansion = CaptureExpansionStates();
MainNavigationView.MenuItems.Clear();
foreach (var profile in _profiles)
{
var navItem = CreateProfileItem(profile);
if (expansion.TryGetValue(profile.Id, out var expanded)) navItem.IsExpanded = expanded;
MainNavigationView.MenuItems.Add(navItem);
}
UpdateProfileIconsForPane(MainNavigationView.IsPaneOpen);
}
private NavigationViewItem CreateProfileItem(GameProfile profile)
{
var item = new NavigationViewItem { Content = new TextBlock { Text = BuildProfileLabel(profile) }, Tag = profile, IsExpanded = true, SelectsOnInvoked = false };
foreach (var sub in profile.SubProfiles)
item.MenuItems.Add(new NavigationViewItem { Content = new TextBlock { Text = BuildSubProfileLabel(sub) }, Tag = sub });
return item;
}
private static string BuildProfileLabel(GameProfile p) => string.IsNullOrWhiteSpace(p.HotKey) ? p.Name : $"{p.Name} ({p.HotKey})";
private static string BuildSubProfileLabel(SubProfile s) => string.IsNullOrWhiteSpace(s.HotKey) ? s.Name : $"{s.Name} ({s.HotKey})";
private static string GetInitials(string name) { var parts = name.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); return parts.Length == 0 ? "?" : parts.Length == 1 ? parts[0][..Math.Min(2, parts[0].Length)].ToUpperInvariant() : $"{parts[0][0]}{parts[1][0]}".ToUpperInvariant(); }
private Dictionary<Guid, bool> CaptureExpansionStates() => MainNavigationView.MenuItems.OfType<NavigationViewItem>()
.Where(i => i.Tag is GameProfile).ToDictionary(i => ((GameProfile)i.Tag).Id, i => i.IsExpanded);
private void UpdateProfileIconsForPane(bool isPaneOpen)
{
foreach (var item in MainNavigationView.MenuItems.OfType<NavigationViewItem>())
{
if (item.Tag is not GameProfile profile) continue;
if (isPaneOpen)
{
item.Icon = null;
item.Content = new TextBlock { Text = BuildProfileLabel(profile), TextTrimming = TextTrimming.CharacterEllipsis };
item.HorizontalContentAlignment = HorizontalAlignment.Stretch;
item.Padding = new Thickness(12, item.Padding.Top, 12, item.Padding.Bottom);
item.Resources.Remove("NavigationViewItemOnLeftIconBoxColumnWidth");
}
else
{
item.Resources["NavigationViewItemOnLeftIconBoxColumnWidth"] = 56d;
item.Icon = new FontIcon { Glyph = GetInitials(profile.Name), FontFamily = new FontFamily("Segoe UI"), FontWeight = Microsoft.UI.Text.FontWeights.SemiBold, FontSize = 30, Width = 64, Height = 32, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center };
item.Content = null;
item.HorizontalContentAlignment = HorizontalAlignment.Center;
item.Padding = new Thickness(0);
}
}
}
private void TogglePaneButton_Click(object sender, RoutedEventArgs e)
{
try { MainNavigationView.IsPaneOpen = !MainNavigationView.IsPaneOpen; StatusText.Text = $"Status: Toggle SUCCESS - Pane is now {(MainNavigationView.IsPaneOpen ? "OPEN" : "CLOSED")}"; }
catch (COMException ex) { StatusText.Text = $"Status: COM EXCEPTION - HResult: 0x{ex.HResult:X8}"; }
}
private void ModifyItemsButton_Click(object sender, RoutedEventArgs e)
{
_profiles.Clear();
var delta = new GameProfile { Name = "Delta Ops", HotKey = "Ctrl+4" }; delta.SubProfiles.Add(new SubProfile { Name = "Assault", HotKey = "F6" }); delta.SubProfiles.Add(new SubProfile { Name = "Stealth", HotKey = "F7" });
var echo = new GameProfile { Name = "Echo Racing", HotKey = "Ctrl+5" }; echo.SubProfiles.Add(new SubProfile { Name = "Drag", HotKey = "F8" });
_profiles.Add(delta); _profiles.Add(echo); _selectedProfile = _profiles.FirstOrDefault();
BuildNavigationItems();
StatusText.Text = "Status: MenuItems refreshed via Clear + Add. Next toggle will throw COM exception.";
}
private void AddProfileButton_Click(object sender, RoutedEventArgs e)
{
var p = new GameProfile { Name = $"Profile {_profiles.Count + 1}", HotKey = $"Ctrl+{_profiles.Count + 6}" }; p.SubProfiles.Add(new SubProfile { Name = "Default" });
_profiles.Add(p); _selectedProfile = p; BuildNavigationItems();
StatusText.Text = "Status: Profile added and NavigationView rebuilt (Clear + Add).";
}
private void AddSubProfileButton_Click(object sender, RoutedEventArgs e)
{
if (_selectedProfile == null) { StatusText.Text = "Status: Select a profile first to add a sub-profile."; return; }
_selectedProfile.SubProfiles.Add(new SubProfile { Name = $"Sub {_selectedProfile.SubProfiles.Count + 1}" });
BuildNavigationItems();
StatusText.Text = "Status: Sub-profile added. NavigationView rebuilt (Clear + Add).";
}
private void MainNavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
if (args.SelectedItem is NavigationViewItem navItem)
{
if (navItem.Tag is GameProfile profile) _selectedProfile = profile;
else if (navItem.Tag is SubProfile && navItem.Parent is NavigationViewItem parent && parent.Tag is GameProfile parentProfile) _selectedProfile = parentProfile;
}
}
private void MainNavigationView_PaneOpened(NavigationView sender, object args) => UpdateProfileIconsForPane(true);
private void MainNavigationView_PaneClosed(NavigationView sender, object args) => UpdateProfileIconsForPane(false);
}Repro steps:
- Create a new WinUI 3 Desktop app (e.g.,
dotnet new winui3 -n NavigationViewBugRepro, .NET 8/9, WinAppSDK 1.8.3). - Replace the generated MainWindow.xaml and MainWindow.xaml.cs with the snippets above (plus the two model classes).
- Run the app (title: "NavigationView COM Exception Repro").
- Click "Toggle Pane" -> works.
- Click "Refresh MenuItems (Clear + Add)" -> rebuilds NavigationView items with profiles/sub-profiles.
- Click "Toggle Pane" again -> COMException (0x80004005) thrown immediately by
MainNavigationView.IsPaneOpen = !MainNavigationView.IsPaneOpen;. - Repeat toggles -> exception persists until another rebuild; another rebuild temporarily "fixes" it, but the next toggle after rebuild breaks again.
Actual behavior
After calling MenuItems.Clear() followed by Add() operations:
- Setting MainNavigationView.IsPaneOpen = !MainNavigationView.IsPaneOpen throws COMException
(0x80004005) - The exception is thrown immediately when the property setter is called
- The NavigationView appears visually normal but is internally corrupted
- All subsequent toggle attempts fail with the same exception until MenuItems is modified again
- The pane remains stuck in its current state (open or closed)
- No WinUI events fire (PaneOpening, PaneOpened, PaneClosing, PaneClosed)
The NavigationView's internal SplitView appears to have a broken width binding that gets stuck at the compact width (40-60px) even when it should expand to 250px, causing the animation system to fail.
Expected behavior
IsPaneOpen should toggle successfully regardless of whether MenuItems collection has been modified. The NavigationView should:
- Accept IsPaneOpen property changes without throwing exceptions
- Properly update its internal SplitView width bindings after MenuItems changes
- Fire appropriate events (PaneOpening/PaneOpened/PaneClosing/PaneClosed)
- Maintain functional state across MenuItems refresh operations
The MenuItems refresh operation should not corrupt the NavigationView's pane toggle functionality.
Screenshots
Currpted state screenshots (The last 2 images should have included arrows to expand the profiles and display their sub-profiles.):
Expected behaviour screenshots :
Console output showing exception:
System.Runtime.InteropServices.COMException (0x80004005): Unspecified error
at ABI.Microsoft.UI.Xaml.Controls.INavigationViewMethods.set_IsPaneOpen(IObjectReference
_obj, Boolean value)
at Microsoft.UI.Xaml.Controls.NavigationView.set_IsPaneOpen(Boolean value)
NuGet package version
WinUI 3 - Windows App SDK 1.8.3: 1.8.251106002
Windows version
Windows Insider Build (xxxxx), Windows 11 (24H2): Build 26100
Additional context
Debugging and Attempted Workarounds
I have spent significant time investigating this issue and attempted every possible workaround
without success:
Attempt 1: Use MenuItemsSource with ObservableCollection
// Instead of MenuItems.Clear() + Add()
var newCollection = new ObservableCollection();
// ... populate newCollection ...
MainNavigationView.MenuItemsSource = newCollection;
// Result: Still throws COM exception
Related issues: #2818, #8770
Attempt 2: Clear MenuItemsSource before reassigning
MainNavigationView.MenuItemsSource = null;
await Task.Delay(50);
MainNavigationView.MenuItemsSource = newCollection;
// Result: Still throws COM exception
Attempt 3: Use RemoveAt() in reverse order instead of Clear()
// Avoid Clear() to prevent binding corruption (related to #1178)
for (int i = MainNavigationView.MenuItems.Count - 1; i >= 0; i--)
{
MainNavigationView.MenuItems.RemoveAt(i);
}
// ... then Add() new items
// Result: Still throws COM exception
Related issue: #1178 - SplitView width binding corruption
Attempt 4: Async dispatch toggle with DispatcherQueue
await DispatcherQueue.EnqueueAsync(() => {
MainNavigationView.IsPaneOpen = !MainNavigationView.IsPaneOpen;
});
// Result: Still throws COM exception
Attempt 5: Add delays before toggling
ModifyNavigationItems();
await Task.Delay(1000); // Wait 1 second
MainNavigationView.IsPaneOpen = !MainNavigationView.IsPaneOpen;
// Result: Exception persists even after 1+ second delays
Attempt 6: Force layout updates
MainNavigationView.IsPaneOpen = !MainNavigationView.IsPaneOpen;
MainNavigationView.InvalidateMeasure();
MainNavigationView.InvalidateArrange();
MainNavigationView.UpdateLayout();
// Result: Exception thrown BEFORE these methods execute
Attempt 7: Rebuild NavigationView on exception
catch (COMException)
{
// Try to recover by rebuilding MenuItems
ModifyNavigationItems();
await Task.Delay(100);
MainNavigationView.IsPaneOpen = !MainNavigationView.IsPaneOpen;
}
// Result: The rebuild itself throws the same COM exception
Attempt 8: Hide/show NavigationView to force reset
MainNavigationView.Visibility = Visibility.Collapsed;
await Task.Delay(50);
ModifyNavigationItems();
await Task.Delay(50);
MainNavigationView.Visibility = Visibility.Visible;
await Task.Delay(50);
MainNavigationView.IsPaneOpen = targetState;
// Result: Exception persists even after complete visibility reset
Attempt 9: Use single toggle button (avoid conflicts)
Ensured only TitleBar has IsPaneToggleButtonVisible="True" and NavigationView has
IsPaneToggleButtonVisible="False" to avoid potential conflicts between two toggle buttons.
// Result: No change, still throws exception
Attempt 10: Block toggles during refresh with flags
bool _isRefreshing = false;
void Refresh()
{
_isRefreshing = true;
ModifyNavigationItems();
_isRefreshing = false;
}
void Toggle()
{
if (_isRefreshing) return; // Block toggle during refresh
MainNavigationView.IsPaneOpen = !MainNavigationView.IsPaneOpen;
}
// Result: Exception occurs even with proper blocking
Root Cause Analysis
Based on extensive testing, the issue appears to be:
- SplitView Width Binding Corruption: NavigationView uses an internal SplitView control. After MenuItems modification, the SplitView's column width binding becomes invalid.
- Binding Stuck at Compact Width: The first Grid column width should update from 40-60px (compact) to 250px (expanded) when IsPaneOpen = true, but the binding remains stuck at the compact width.
- Animation System Failure: When IsPaneOpen is set, the pane animation system tries to animate based on the binding, but fails because the width constraint is corrupted, resulting in COM exception from the WinRT layer.
- Permanent Corruption: The corruption persists indefinitely until MenuItems is modified again, which temporarily resets some internal state.
Related Issues
- NavigationView pane doesn't correctly open after navigating to page with enabled cache #1178 - NavigationView pane doesn't correctly open after navigating (SplitView width binding stuck at 40px)
- NavigationView crashes when changing the collection bound to MenuItemsSource #2818 - NavigationView crashes when changing MenuItemsSource collection (marked COMPLETED but issue persists)
- Updating an ObservableCollection bound to a MenuItemsSource crashes with ArgumentOutOfRangeException #8770 - Updating ObservableCollection bound to MenuItemsSource causes ArgumentOutOfRangeException (marked FIXED in WinAppSDK 1.5 but issue persists)
- WinRT originate error - 0x80004005 : 'Unspecified error'. #7700 - WinRT originate error 0x80004005
Affected Apps: Any WinUI 3 application using NavigationView with dynamic content that needs to:
- Add/remove/modify navigation items at runtime
- Manage user profiles, categories, or dynamic lists
- Provide CRUD operations that refresh NavigationView
Current Status: No known workaround exists. All attempted solutions fail.
Suggested Fix
The NavigationView should:
- Properly invalidate and rebind its internal SplitView state when MenuItems changes
- Provide a public Refresh() or InvalidateState() method to force state synchronization
- Handle MenuItems.Clear() without corrupting the SplitView binding
- Ensure IsPaneOpen property setter is resilient to internal state corruption
Thank you for investigating this issue!
Metadata
Metadata
Assignees
Labels
Type
Projects
Status