How to mix dynamic and static items in UWP XAML NavigationView MenuItems?

3k views Asked by At

I'm trying to make a NavigationViewMenu and I need a menu layed out as follows

  • static Home item

  • static Header

  • dynamic elements from DB as items

  • static Header

  • static set of items

This is what I tried:

<NavigationView.MenuItems>

    <NavigationViewItem Icon="Home"  Content="Home" Tag="home" />

    <NavigationViewItemSeparator />

    <NavigationViewItemHeader Content="My Stuff"/>

    <NavigationViewList ItemsSource="{x:Bind MyStuff}">
        <NavigationViewList.ItemTemplate>
            <DataTemplate x:DataType="local:MyModel">
                <NavigationViewItem Icon="Pictures" Content="{x:Bind Name}" Tag="{x:Bind Tag}" />
            </DataTemplate>
        </NavigationViewList.ItemTemplate>
    </NavigationViewList>

    <!-- Static equivalent to the above:
    <NavigationViewItem Icon="Pictures" Content="Woop" Tag="foos"/>
    <NavigationViewItem Icon="Pictures" Content="Doop" Tag="foos"/>
    <NavigationViewItem Icon="Pictures" Content="Loop" Tag="foos"/>
    -->

    <NavigationViewItemHeader Content="Other Stuff"/>

    <NavigationViewItem Icon="Pictures" Content="Foos" Tag="foos"/>
    <NavigationViewItem Icon="ContactInfo" Content="Bars" Tag="bars"/>
    <NavigationViewItem Icon="SwitchApps" Content="Bazes" Tag="bazes"/>

</NavigationView.MenuItems>

This is what I've got:

enter image description here

This is what I wanted:

enter image description here

Is there anything as good and practical as Angular's *ngFor in XAML for UWP?

3

There are 3 answers

0
Will On BEST ANSWER

I ran into the same behavior, and managed to find a work around. In my case, I had two lists of menu items (dynamically data-bound items), and I wanted to use NavigationViewItemHeader on top of both (static items). I tried using a NavigationViewList and ran into your problem.

TL;DR:

Create a list of menu items in C# code. The elements of this list can be a mix of your viewmodels, and any static Navigation Items (headers, separators, etc). Then use a DataTemplateSelector to either databind to your viewmodel or pass-through the navigation items unchanged.

More detailed

In your C# code-behind, create an enumerable (or observable collection) of your menu items. In my case SomeCollection and AnotherCollection represent my data sources that I wanted to bind to my NavigationView. I have to type it as object because it's a mix of my viewmodels and the built-in UWP navigation item types.

private IEnumerable<object> MenuItems()
{
    yield return new NavigationViewItemHeader { Content = "Some List" };
    foreach (var some in SomeCollection)
    {
        yield return some;
    }
    yield return new NavigationViewItemHeader { Content = "Another List" };
    foreach (var another in AnotherCollection)
    {
        yield return another;
    }
}

// somewhere else, like in your Page constructor or a CollectionChanged handler
this.NavigationList = MenuItems().ToList();

Second, create a Data Template Selector to switch between your template and the navigation items:

class NavigationItemTemplateSelector : DataTemplateSelector
{
    public DataTemplate ViewModelTemplate{ get; set; }
    public DataTemplate NavigationItemTemplate { get; set; }
    protected override DataTemplate SelectTemplateCore(object item)
    {
        return item is MyViewModel
            ? ViewModelTemplate
            : NavigationItemTemplate;
    }
}

Finally, change your NavigationView to reference the template selector and menu item source. The NavigationItemTemplate is just a pass-through, and your ViewModelTemplate would have the normal viewmodel item binding logic.

<Page.Resources>
    <DataTemplate x:Key="ViewModelTemplate" x:DataType="local:MyViewModel">
        <TextBlock Text="{x:Bind SomeProperty}" />
    </DataTemplate>
    <DataTemplate x:Key="NavigationItemTemplate">
    </DataTemplate>
    <local:NavigationItemTemplateSelector x:Key="NavigationItemTemplateSelector"
        ViewModelTemplate="{StaticResource ViewModelTemplate}"
        NavigationItemTemplate="{StaticResource NavigationItemTemplate}" />
</Page.Resources>


<NavigationView
    MenuItemsSource="{x:Bind NavigationList, Mode=OneWay}"
    MenuItemTemplateSelector="{StaticResource NavigationItemTemplateSelector}">
    <Frame x:Name="ContentFrame"></Frame>
</NavigationView>
0
Gábor On

I didn't find it necessary to use different templates as in the accepted answer, maybe because there were some changes in the underlying Windows code in the meantime. As I needed a stable part of the menu and then a dynamic part depending on the actual page, I created an interface:

interface IMenuProvider {
  IEnumerable<NavigationViewItemBase> GetMenuItems();
}

and made sure all my pages implement it. My MainPage returns the fixed part:

public IEnumerable<NavigationViewItemBase> GetMenuItems() {
  yield return new NavigationViewItem {
    Tag = "home",
    Icon = new SymbolIcon(Symbol.Home),
    Content = "Home",
  };
  yield return new NavigationViewItemSeparator();
  yield return new NavigationViewItem {
    Tag = "xxx",
    Icon = new SymbolIcon(Symbol.XXX),
    Content = "XXX",
  };
}

the other pages, similary, provide their own menu headers and items.

When I navigate the pages, I change the menu as well, concatenating the fixed and variable parts:

ContentFrame.Navigate(PageType, null, transitionInfo);
if (ContentFrame.Content is IMenuProvider menuProvider)
  = GetMenuItems().Concat(menuProvider.GetMenuItems()).ToList();

(Or, you might place the menu change into the Navigated handler of the Frame.)

While it's still a nuisance that these menus, at least the fixed part, cannot be declared in XAML, this approach works.

1
Barry Wang On

I can reproduce it. It looks like NavigationViewList only take the space of one item when putting itself in NavigationView.MenuItem. Which is the same like putting a ListView in a ListViewItem. To change this behavior we need to change the item's behaviour ourselves. However after some investigating it seems currently customization of NavigationViewList is blackbox for us. So the only way I could think is to build our own NavigationView with the help of splitview and acrylic.