Kinnara / ModernWpf

Modern styles and controls for your WPF applications
MIT License
4.45k stars 446 forks source link

NavigationView breaks when using MenuItemTemplateSelector and more than 4 items in MenuItemsSource #111

Open HadriMX opened 4 years ago

HadriMX commented 4 years ago

Weird behavior, but there seems to be a loop in SelectTemplate method from MenuItemTemplateSelector only when there are more than 4 items in MenuItemsSource.

ShankarBUS commented 4 years ago

I doesn't happen to me, I have more than 12 nav menu items and 4 distinct templates for them. Can you elaborate your issue with some sample code?

HadriMX commented 4 years ago

These are my resources, two DataTemplates and one Selector.

    <Window.Resources>
        <DataTemplate x:Key="MainNavigationViewTemplate">
            <ui:NavigationViewItem Content="{Binding DisplayName, Mode=OneTime}" UseSystemFocusVisuals="True">
                <ui:NavigationViewItem.Icon>
                    <ui:FontIcon Glyph="{Binding Glyph, Mode=OneTime}" />
                </ui:NavigationViewItem.Icon>
            </ui:NavigationViewItem>
        </DataTemplate>

        <DataTemplate x:Key="MainNavigationViewPendientesTemplate">
            <ui:NavigationViewItem UseSystemFocusVisuals="True">
                <ui:NavigationViewItem.Icon>
                    <ui:FontIcon Glyph="{Binding Glyph, Mode=OneTime}" />
                </ui:NavigationViewItem.Icon>

                <ui:NavigationViewItem.Content>
                    <mah:Badged Badge="{Binding TotalPendientesCount, FallbackValue={x:Null}}"
                                BadgeBackground="{DynamicResource SystemControlBackgroundAccentBrush}"
                                BadgeForeground="{DynamicResource SystemControlForegroundBaseHighBrush}"
                                BadgePlacementMode="Right">
                        <TextBlock Text="{Binding DisplayName, Mode=OneTime}" />
                    </mah:Badged>
                </ui:NavigationViewItem.Content>
            </ui:NavigationViewItem>
        </DataTemplate>

        <s:MainNavigationViewItemTemplateSelector x:Key="MyDataTemplateSelector"
                                                  DefaultTemplate="{StaticResource MainNavigationViewTemplate}"
                                                  PendientesTemplate="{StaticResource MainNavigationViewPendientesTemplate}" />
    </Window.Resources>

My DataTemplateSelector:

public class MainNavigationViewItemTemplateSelector : DataTemplateSelector
    {
        public DataTemplate DefaultTemplate { get; set; }
        public DataTemplate PendientesTemplate { get; set; }

        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            Log(item);    // here I write log to VS output window and practically it gets flooded, also UI thread seems to get so busy that app becomes totally unresponsive
            var mainMenuItem = (IMainMenuItem)item;
            return mainMenuItem.DisplayName != "Pendientes" ? DefaultTemplate : PendientesTemplate;
        }
    }

My NavigationView:

<ui:NavigationView x:Name="MainNavigationView"
                           IsBackButtonVisible="Collapsed"
                           IsSettingsVisible="True"
                           IsTabStop="False"
                           ItemInvoked="MainNavigationView_ItemInvoked"
                           MenuItemTemplateSelector="{StaticResource MyDataTemplateSelector}"
                           MenuItemsSource="{Binding MainMenuItems, Mode=OneWay}"
                           OpenPaneLength="240"
                           PaneDisplayMode="Left"
                           SelectionFollowsFocus="Enabled">
...
</ui:NavigationView>

My source collection (MenuItemsSource):

public ObservableCollection<IMainMenuItem> MainMenuItems { get; set; } = new ObservableCollection<IMainMenuItem>();

My interface IMainMenuItem:

public interface IMainMenuItem
    {
        bool CanGoBack { get; set; }
        string DisplayName { get; }
        string Glyph { get; }
    }

Note: I try to use MVVM whenever I can, but mostly I use code-behind (hehe).

ShankarBUS commented 4 years ago

It does get broken 😅. It takes a while for rendering the icons and becomes unresponsive. But this didn't happen with mine even though I have multiple items

ShankarBUS commented 4 years ago

Hey @Kinnara, I can reproduce this issue (see ShankarBUS/ModernWpfTest repo). I can't find what's causing this 😑. This is causing too much startup delay.

More Infos

And it's up-to you 😑

Kinnara commented 4 years ago

Thank you @HadriMX and @ShankarBUS. Below is a quick workaround for left nav. Top nav has a similar issue (actually worse) that seems to have a different cause and is not fixable by this workaround. Needs more investigating.

public class NavView : NavigationView
{
    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        if (GetTemplateChild("MenuItemsHost") is ItemsRepeater leftNavRepeater)
        {
            leftNavRepeater.Layout = new NonVirtualizingStackLayout();
        }
    }
}
public class NonVirtualizingStackLayout : NonVirtualizingLayout
{
    #region Orientation

    public static readonly DependencyProperty OrientationProperty =
        DependencyProperty.Register(
            nameof(Orientation),
            typeof(Orientation),
            typeof(NonVirtualizingStackLayout),
            new PropertyMetadata(Orientation.Vertical, OnOrientationChanged));

    public Orientation Orientation
    {
        get => (Orientation)GetValue(OrientationProperty);
        set => SetValue(OrientationProperty, value);
    }

    private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((NonVirtualizingStackLayout)d).InvalidateMeasure();
    }

    #endregion

    protected override Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize)
    {
        Size stackDesiredSize = new Size();
        var children = context.Children;
        Size layoutSlotSize = availableSize;
        bool fHorizontal = Orientation == Orientation.Horizontal;

        if (fHorizontal)
        {
            layoutSlotSize.Width = double.PositiveInfinity;
        }
        else
        {
            layoutSlotSize.Height = double.PositiveInfinity;
        }

        for (int i = 0, count = children.Count; i < count; ++i)
        {
            UIElement child = children[i];

            if (child == null) { continue; }

            child.Measure(layoutSlotSize);
            Size childDesiredSize = child.DesiredSize;

            if (fHorizontal)
            {
                stackDesiredSize.Width += childDesiredSize.Width;
                stackDesiredSize.Height = Math.Max(stackDesiredSize.Height, childDesiredSize.Height);
            }
            else
            {
                stackDesiredSize.Width = Math.Max(stackDesiredSize.Width, childDesiredSize.Width);
                stackDesiredSize.Height += childDesiredSize.Height;
            }
        }

        if (fHorizontal)
        {
            if (double.IsFinite(availableSize.Height))
            {
                stackDesiredSize.Height = Math.Max(stackDesiredSize.Height, availableSize.Height);
            }
        }
        else
        {
            if (double.IsFinite(availableSize.Width))
            {
                stackDesiredSize.Width = Math.Max(stackDesiredSize.Width, availableSize.Width);
            }
        }

        return stackDesiredSize;
    }

    protected override Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize)
    {
        var children = context.Children;
        bool fHorizontal = Orientation == Orientation.Horizontal;
        Rect rcChild = new Rect(finalSize);
        double previousChildSize = 0.0;

        for (int i = 0, count = children.Count; i < count; ++i)
        {
            UIElement child = children[i];

            if (child == null) { continue; }

            if (fHorizontal)
            {
                rcChild.X += previousChildSize;
                previousChildSize = child.DesiredSize.Width;
                rcChild.Width = previousChildSize;
                rcChild.Height = Math.Max(finalSize.Height, child.DesiredSize.Height);
            }
            else
            {
                rcChild.Y += previousChildSize;
                previousChildSize = child.DesiredSize.Height;
                rcChild.Height = previousChildSize;
                rcChild.Width = Math.Max(finalSize.Width, child.DesiredSize.Width);
            }

            child.Arrange(rcChild);
        }
        return finalSize;
    }
}
ShankarBUS commented 4 years ago

One thing that's confusing me is that this doesn't happen with my project (private), I have more than 12 dynamically generated nav menu items and 4 distinct (I guess -> header, separater, default navitem and hierarchical navitem) templates for them. What is the particular cause?

Kinnara commented 4 years ago

The issue seems no longer reproducible in left mode with the latest master version. In top mode it's still broken. WinUI 2.5 has the same issue.