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

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:
This is what I wanted:
Is there anything as good and practical as Angular's *ngFor in XAML for UWP?

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>

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.

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.

Related

Localize strings in XAML UI in UWP

I have a resource entry named info_278 in the Resources.resw file on my UWP app. I have 3 scenarios where I need to use this resource but looks like I need to duplicate this to cater to different scenarios. Scenarios are as follows.
Error message content from code
var displayErrorOnPopup = ResourceHandler.Get("info_278");
TextBlock Text property from XAML (Looks like a new entry needed as info_278.Text)
<TextBlock x:Uid="info_278" Margin="10,0,0,0" />
Button Content property from XAML (Looks like a new entry needed as info_278.Content)
<Button x:Uid="info_278" Margin="10,0,0,0" />
How do I proceed without duplicating this resource in the .resw file?
The only way to avoid duplication is to set the string value in code-behind using ResourceLoader. Because you could direct access to the specific property of the target control. Like this:
var resourceLoader = Windows.ApplicationModel.Resources.ResourceLoader.GetForCurrentView();
this.TextBlock.Text = resourceLoader.GetString("info_278");
If you are not going to do it in the code behind, then I have to say there is no way to avoid the duplication of the resource string. You should add info_278.Text and info_278.Content for different XAML scenarios.
You could create a markup extension. I've used this in WinUI 3, but should work in UWP too.
using Microsoft.UI.Xaml.Markup;
using Windows.ApplicationModel.Resources;
namespace MyApp;
[MarkupExtensionReturnType(ReturnType = typeof(string))]
public class StringResourceExtension : MarkupExtension
{
private static readonly ResourceLoader _resourceLoader = new();
public StringResourceExtension() { }
public string Key { get; set; } = "";
protected override object ProvideValue()
{
return _resourceLoader.GetString(Key);
}
}
Then in the XAML:
...
local="using:MyApp"
...
<TextBlock Text="{local:StringResource Key=info_278}" />
<Button Content="{local:StringResource Key=info_278}" />
The Content of Button can be a TextBlock:
<Button>
<TextBlock x:Uid="MyTextId" Style="{StaticResource MyTextBlockStyle}" />
</Button>

How to use Xamarin to call an UI Element with a bound name

I'm trying to use a custom variable to name UI Elements in XAML in order to use them in my Xamarin code. How would I do that?
I know that I can use the tags
<Label x:Name="CallVariable" Text="This will appear"/>
and I can call the label from Xamarin using
Label foo = FindByName("CallVariable") as Label;
and mess with the text with the following
foo.Text == "This will appear"
which should return true.
I learned about binding, and so I tried to use this in my variables.
<Label x:Name={Binding Name}/>
Label bar = FindByName(emp.training[i] as Label);
Unfortunately, every time I run it, I'm receiving the error:
System.NullReferenceException: Object reference not set to an instance of an object.
I remembered to set the BindingContext. And in case it's important, this is all happening within a list view.
Is x:Name a bindable object? Or is there another method I should be using? Maybe a way to call an object based on its label, or something?
My current task is as follows:
I have a list of people's information. Name, Age, Gender, Email, Location, and a few other pieces of information. I'm trying to get someone to search for certain types of people, have it return a list of everyone in a ListView, and have the person be able to select as many as they want. I did this using a button that adds the person to a list (or removes them from the list if they're already on). If the user chooses to, they should be able to also click "Select All" to add everyone. I've got the individual adding down, I just need to somehow select all of them.
You can't access the label with a listView by x:Name in code behind. Instead of access the label in the listView, you can bind the property of label in code behind. For example:
<ListView x:Name="testListView">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout>
<Label Text="{Binding Name}" />
<Label Text="{Binding Desc}" />
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
And in code behind, it should be:
public partial class MainPage : ContentPage
{
ObservableCollection<testViewModel> myModels = new ObservableCollection<testViewModel>();
testViewModel model;
public MainPage()
{
InitializeComponent();
testListView.ItemsSource = myModels;
myModels.Add(new testViewModel { Name = "age" });
myModels.Add(new testViewModel { Name = "gender" });
myModels.Add(new testViewModel { Name = "name" });
}
}
class testViewModel
{
public string Name { get; set; }
public string Desc { get; set; }
}
Next time you want to update the text of label:
public void test() {
//Get the model you want to change
model.Desc = "This will appear";
}
I've got the individual adding down, I just need to somehow select
all of them.
I don't know how you implement the individual adding down, share the code and maybe I can give you some suggestions.

CompositeCollection containing an ICollectionView

I'm trying to implement a tab control, where each item comes from an ICollectionView of my viewmodel. Each tab page, for the items from the ICollectionView will be the same. However, I would like there to be an extra tab page for configuration options.
So an example tab header 'screenshot' might be:
tabA | tabB | tabC | config
on another instance, it could be
tabA | config
or
config
I can define the header for each item using ItemTemplateSelectors, and the content using the ContentTemplateSelectors. So that bit should be okay.
I'm having trouble with adding the config page item since I do not know where to add it. I thought I could set the tab's ItemsSource to be a CompositeCollection, where the final item is the config page object. I have failed to achieve this.
In the following example, I can view the tab headers being populated correctly according to the designer sample data which I have set up - I have not yet added the config page.
<controls:MetroTabControl ItemsSource="{Binding View}">
<controls:MetroTabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Value.siteDisplayName}" />
</DataTemplate>
</controls:MetroTabControl.ItemTemplate>
<controls:MetroTabControl.ContentTemplate>
<DataTemplate>
<TextBlock Text="{Binding Value.siteComment}"/>
</DataTemplate>
</controls:MetroTabControl.ContentTemplate>
</controls:MetroTabControl>
As you see, I have set the ItemsSource to be {Binding View}. This "View" comes from my ViewModel and is an ICollectionView.
Ideally i'd be able to do some magic like:
<controls:MetroTabControl>
<controls:MetroTabControl.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding View}"/>
<SomeConfigPageObject/>
</CompositeCollection>
</controls:MetroTabControl.ItemsSource>
...snip...
</controls:MetroTabControl>
But the problem is that when I do the above, the designer preview of the control acts as if there are no items in the ItemsSource.
For reference, each item in the {Binding View} is a object which contains a Value property, the value property containing an object that contains, in this example, a siteDisplayName and siteComment.
For reference, the DataContext for the tab is defined the dockpanel that contains it, as follows.
<DockPanel DataContext="{Binding Source={StaticResource Configurator}}"
d:DataContext="{d:DesignInstance cfuid:ConfigSiteVMSampleData, IsDesignTimeCreatable=true}"
LastChildFill="True">
For reference, the Configurator is my viewmodel and is instantiated in the xaml as:
<UserControl.Resources>
<ResourceDictionary>
...snip...
<cfvmc:ConfigSiteVM x:Key="Configurator" />
...snip...
So, the actual question would be:
How do I add my "config page" at the end of the tab control? Preferably via using the above-hoped method of adding an extra config-page object on the CompositeCollection; however if this is not possible [1] i'm open for suggestions.
[1] I think it doesn't work because the {Binding View} is an ICollectionView and the CompositeCollection requires a "collection" and doesn't accept a "view"
Thank you.
Peter.
I decided to do it through code behind. This means that I do lose my ability to use the design-time data to preview my UI; but it works at run time.
So, in the xaml I have.
<controls:MetroTabControl Grid.Column="0" Grid.ColumnSpan="2"
Grid.Row="0" Grid.RowSpan="2"
ItemsSource="{Binding ElementName=ucMe, Path=TabSitesCollection}">
Where ucMe is the UserControl and TabSitesCollection is a
protected CollectionViewSource m_TabSitesCollectionViewSource;
protected CompositeCollection m_TabSitesComposites;
public ICollectionView TabSitesCollection
{
get { return m_TabSitesCollectionViewSource.View; }
}
That gets initialised in the constructor as follows
public ConfigSiteView()
{
m_TabSitesComposites = new CompositeCollection();
m_TabSitesCollectionViewSource = new CollectionViewSource();
m_TabSitesCollectionViewSource.Source = m_TabSitesComposites;
InitializeComponent();
}
Then, on the Loaded event I can do
m_TabSitesComposites.Add(new CollectionContainer() { Collection = GetModel.View });
m_TabSitesComposites.Add(new TabItem() { Header = "hi" });
m_TabSitesComposites.Add(new TabItem() { Header = "ho" });
This results in almost my desired UI
I now simply need to spiff up my settings tab item and i'm done.
For reference, the xaml designer does not have any preview data - Unless I change the xaml so that the preview loads up (which then breaks the actual execution)
It would have been nice to have it both work while running, and on preview, but I haven't figured out how to do that, and it's not a current priority.

Dynamically change DataTemplate for a ListView at Runtime

I have 2 DataTemplates for displaying the contents of ClassA or ClassB inside a single ListView; which template to select will be based on a RadioButton selection by the user.
Is it possible to change the ItemTemplate of a ListView (in XAML) based on user input dynamically at runtime?
An example snippet of code:
XAML Page:
<Page...>
<Page.Resources>
<DataTemplate x:Key="ClassAListViewItemTemplate" x:DataType="vm:ClassA" ... />
<DataTemplate x:Key="ClassBListViewItemTemplate" x:DataType="vm:ClassB" ... />
</Page.Resources>
<RelativePanel>
<RadioButton Content="ClassA" ... />
<RadioButton Content="ClassB" ... />
<ListView DataContext="{Binding Path=MainViewModel}"
ItemsSource="{Binding ListOfClassAOrB, Mode=TwoWay}"
ItemTemplate="{StaticResource ClassAListViewItemTemplate}"/>
</RelativePanel>
</Page>
I have stripped the code down somewhat to the essentials, but I would like to be able to change the following at runtime:
ItemTemplate="{StaticResource ClassAListViewItemTemplate}"
I have seen solutions for Classic WPF applications that use Style.Triggers, but these aren't applicable for UWP
Marco Minerva's blog on Adaptive Triggers, RelativePanel and DataTemplate in the Universal Windows Platform talks of using UserControls within DataTemplates to modify the visual state using Adaptive Triggers, but this doesn't take into account switching out of templates based on user input
The closest answer I have found to my problem is another blog he wrote "Dynamically choose DataTemplate in WinRT" where there is an element of code-behind involved - but it only appears to be an if statement - but its the cleanest solution I have come across thus far, and what I'd like to replicate in XAML
Thanks
you need to use overwrite SelectTemplateCore of Data template. Change your view model like this.
Below code will helps you.
public class SampleViewModel : DataTemplateSelector
{
public DataTemplate ClassAListViewItemTemplate{ get; set; }
public DataTemplate ClassBListViewItemTemplate{ get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
var itemsData = item as SampleClass; // add your Data class
if (itemsData.IsAddButton == false) // define any property to select the datatemplate
{
return ClassAListViewItemTemplate;
}
else
{
return ClassBListViewItemTemplate;
}
}
}
Add your two datatemplates to one key, and give the key to ItemTemplateSelector property in gridview.
<viewModels:SampleViewModel x:Key="FeedbackTempateSelector"
ClassAListViewItemTemplate="{StaticResource ClassAListViewItemTemplate}"
ClassBListViewItemTemplate="{StaticResource ClassBListViewItemTemplate}">
</viewModels:SampleViewModel>

Binding the ZIndex of items in an ItemsControl

Using Caliburn.Micro for a WinRT application, I would like to control the ZIndex of items displays in an ItemsControl.
When a user taps on an item, it should become the topmost element.
<ItemsControl Background="White" Height="auto" Width="auto" x:Name="Parts"
HorizontalAlignment="Left"
VerticalAlignment="Top"
>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas></Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
The viewmodel bound to the view above contains a property Parts:
private BindableCollection<IPartViewModel> _parts = new BindableCollection<IPartViewModel>();
public BindableCollection<IPartViewModel> Parts
{
get { return _parts; }
set { _parts = value; NotifyOfPropertyChange(() => Parts); }
}
IPartViewModel has different implementations, each with their own View (= custom user controls).
Every implementation of IPartViewModel has a ZIndex property, ready to be bound.
All other bindings (labels, the tapped event, ...) work perfectly, but I cannot figure out what the binding should look like to control the ZIndex.
Many other questions on SO deal with this issue, but none for WinRT.
I ended up achieving this with a custom ItemsControl and overriding the method GetContainerForItemOverride where I return a ContentPresenter with a binding added to the ZIndex property
protected override DependencyObject GetContainerForItemOverride()
{
var cp = new ContentPresenter();
cp.SetBinding(Canvas.ZIndexProperty, new Binding { Path = new PropertyPath("ZIndex") });
return cp;
}