CompositeCollection containing an ICollectionView - xaml

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.

Related

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

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.

Binding to xaml page's DataContext in Windows Universal App

I'm using a content dialog do display instance data when an item in a grid is selected.
In the calling page's view model, when an item is selected the following method is executed.
public virtual void ItemSelected(object sender, object parameter)
{
var arg = parameter as Windows.UI.Xaml.Controls.ItemClickEventArgs;
var clickedItem = arg.ClickedItem;
var item = clickedItem as ItemsModel;
var dialog = new ItemsDialog();
dialog.DataContext = item;
dialog.ShowAsync();
}
This shows the dialog, and the content is displayed as expected. Now I'm trying to split my xaml into different templates and I'm trying to use a ContentControl to display the appropriate template. I've written a DataTemplateSelector to help choose the correct template, but now I cannot figure out the data binding for the ContentControl (see simplified version below).
<ContentDialog.Resources>
<UI:MyTemplateSelector x:Key="MyTemplateSelector"
Template1="{StaticResource Template1}"
Template2="{StaticResource Template2}"/>
<DataTemplate x:Key="Template1"/>
<DataTemplate x:Key="Template2"/>
</ContentDialog.Resources>
<StackPanel>
<ContentControl DataContext="{Binding}"
ContentTemplateSelector="{StaticResource MyTemplateSelector}"/>
</StackPanel>
When debugging into my ContentTemplateSelector, my binding is always null. I've tried various forms of the binding syntax with no luck. How do I properly set the DataContext of the ContentControl to that of the ContentDialog?
When debugging into my ContentTemplateSelector, my binding is always
null
You need to set data binding for the Content property of ContentControl control, see Remarks in MSDN:
The Content property of a ContentControl can be any type of object,
such as a string, a UIElement, or a DateTime. By default, when the
Content property is set to a UIElement, the UIElement is displayed in
the ContentControl. When Content is set to another type of object, a
string representation of the object is displayed in the
ContentControl.
So the following xaml should work:
<StackPanel>
<ContentControl Content="{Binding}"
ContentTemplateSelector="{StaticResource MyTemplateSelector}"/>
</StackPanel>
Check my completed sample in Github
You have to bind Content also.
Content="{Binding}"
You have the data source (DataContext) and how the data is displayed (templates) and now you need to specify which of the properties brings that together.

Using x:Bind inside the GridView's ItemTemplate layout User Control in UWP

In the Universal Windows Platform API, how do I use x:Bind inside of a User Control (intended to be the layout for a GridView's ItemTemplate) to bind to instance properties of a GridView's ItemSource?
Background
I'm trying to re-create the layout found in Windows 10 stock apps like Sports, News, Money, etc.
I'm using a two GridViews for the main area of the app; one for "featured articles" (2 large photos w/ headlines) and one for all the other articles (smaller photos w/ headlines).
I'm able to bind to a data source that I supply in the code behind (a List where NewsItem is a POCO with a Image and Headline property) Here's the pertinent parts of the MainPage.xaml:
<Page ...
xmlns:data="using:NewsApp.Models" />
....
<GridView Name="FeaturedItems" Grid.Row="0">
<GridView.ItemTemplate>
<DataTemplate x:DataType="data:NewsItem">
<Grid Name="mainPanel" HorizontalAlignment="Stretch" Width="500" >
<Image Source="{x:Bind Image}" HorizontalAlignment="Stretch" />
<TextBlock Text="{x:Bind Headline}" />
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
....
The Image and Headline bind just fine (even though they've not been styled correctly). However, instead I think I need to bind to a User Control to get the styling options I want, control over resizing esp. when using Visual State Triggers and to simplify the XAML in general (at least, this was the technique suggested to me.)
So, I added a new User Control to the project (FeaturedItemControl.xaml), and copied in the DataTemplate's child Grid:
<UserControl ... >
<Grid Name="mainPanel" HorizontalAlignment="Stretch" Width="500" >
<Image Source="{x:Bind Image}" HorizontalAlignment="Stretch" />
<TextBlock Text="{x:Bind Headline}" />
</Grid>
</UserControl>
And then back in the MainPage.xaml, I change the DataTemplate to reference the new FeaturedItemControl:
<GridView Name="FeaturedItems" Grid.Row="0">
<GridView.ItemTemplate>
<DataTemplate x:DataType="data:NewsItem">
<local:FeaturedItemControl HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
However, I get the error message for both Image and Headline properties: Invalid binding path 'Headline': Property 'Headline' can't be found on type 'FeaturedItemControl'.
I've tried a few things but am flailing just throwing code at the problem without understanding what I'm doing. Any help would be greatly appreciated.
Thank you for your kind attention.
Using Depechie's answer, I formulated this little cheat cheat for posterity:
Do note that you MUST use this technique to utilize the VisualStateManager with items inside your data bound controls' (GridView, ListView) data templates.
1) Create a User Control.
2) Cut the content of the DataTemplate in your page and paste it into the User Control replacing the template's Grid.
3) Reference the User Control from inside the Data Template:
4) Modify the contents of the User Control changing x:Bind statements to utilize object.property notation:
<UserControl>
<StackPanel>
<Image Source="{x:Bind NewsItem.LeadPhoto}" />
<TextBlock Text="{x:Bind NewsItem.Headline}" />
<TextBlock Text="{x:Bind NewsItem.Subhead}" />
</StackPanel>
</UserControl>
5) Add this in the User Control's Code Behind:
public Models.NewsItem NewsItem { get { return this.DataContext as Models.NewsItem; } }
public ContactTemplate()
{
this.InitializeComponent();
this.DataContextChanged += (s, e) => Bindings.Update();
}
Well it's possible to use x:Bind in user controls, but you'll need to add some extra code behind.
I encountered the same problem in my project, you can see the result here : https://github.com/AppCreativity/Kliva/tree/master/src/Kliva/Controls
So what you need to do is, create a property in the code behind of your user control that points to the correct DataContext.
If you do that, you can use properties of that DataContext in the xaml of your control: for example:
Do note that in the constructor of your control you do need to add: DataContextChanged += (sender, args) => this.Bindings.Update(); because the datacontext will change depending on the page where your control is used!
Then on the page where you are placing this control, you'll also need to do the same to enable the x:bind to work.
You'll see this in my example on the MainPage.DeviceFamily-Mobile.xaml and MainPage.xaml.cs files.
Hope this helps.
x:Bind isn't really hierarchical like Binding/DataContext is. Additionally when you're not directly inside a DataTemplate (such as inside your user control) the object that x:Bind tries to use is 'this' rather than 'this.DataContext'. My current line of thinking on how to solve this sort of issue is to try not to use UserControls anywhere. Instead preferring DataTemplates contained within a ResourceDictionary. There are some pretty strong caveats to this approach though, you will for example crash the xaml compiler if you use x:Bind inside a data template that was created from the ResourceDictionary item template (add new item). you can find a pretty complete example here https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/XamlBind its important to note in the sample where they show the ResourceDictionary being used that its not actually just a ResourceDictionary.xaml its also a ResourceDictionary.xaml.cs (this is where the generated code from x:Bind ends up)
Another option is to add Headline and Image as properties on your user control and x:Bind them from the template, then inside the user control x:Bind as you are currently doing, but now the x:Bind generated path 'this.Headline' will exist. Unfortunately the order things are actually bound means that the x:Bind's you have inside your user control will have to be OneWay rather than the default OneTime. this is because x:Bind OneTime does the bind inside the InitializeComponent call, and any set of properties/DataContext stuff doesn't get done until after that has already run.
So to sum this up, you have two options, use data templates everywhere, or bind to properties that are directly on the user control.

CollectionViewSource "Value does not fall within the expected range."

Why does this code produce the error in a Windows 8 XAML application?
Value does not fall within the expected range.
The XAML:
<SemanticZoom>
<SemanticZoom.ZoomedInView>
<ListView
Style="{StaticResource HorizontalListViewStyle}"
SelectionMode="None"
ScrollViewer.IsHorizontalScrollChainingEnabled="False"
ItemsSource="{Binding BoardItems}"
ItemContainerStyle="{StaticResource ZoomedOutListViewItemContainerStyle}"
...
The MVVM code:
ObservableCollection<WritingBoardModel> boards = new ObservableCollection<WritingBoardModel>();
... // Add item models to boards.
CollectionViewSource v = new CollectionViewSource()
{
Source = boards,
};
this.ViewModel.Add(BoardItemsViewModelKey, v);
If I skip the CollectionViewSource and directly add the boards instance to my view model, then it all works.
I think I need to use a CollectionViewSource in order to get some semantic zoom selection behaviour to work.
So, CollectionViewSources are weird and the way you have to bind to them is weird as well. To give you an example, in order to do it 'by the book' (the way the sample projects do), I've found it practically has to be a StaticResource as such:
<Page.Resource>
<CollectionViewSource Source="{Binding Whatev}"
x:Key="WhatevSource"/>
</Page.Resource>
<GridView ItemsSource="{Binding Source={StaticResource WhatevSource}}"/>
Notice that we're not setting the source directly to the CollectionViewSource, but we're setting a 'pathless' Binding, basically using the CollectionViewSource as a DataContext (just one way to think of it, not actually technically correct).
This is the only way I've been able to get it to work, though I believe you can technically in the codebehind set the ItemsSource directly to the View of the CollectionViewSource or something similar.
In your Listview add "StaticResource" and "Source"
<ListView ItemsSource="{Binding Source={StaticResource WhatevSource}}"/>
I needed to bind to the View property of the CollectionViewSource like this:
CollectionViewSource v = new CollectionViewSource()
{
IsSourceGrouped = false,
Source = boards,
};
this.ViewModel.Add(BoardItemsViewModelKey, v.View);
Mind you, this doesn't help with my two ListViews and keeping them in selection synch in a SemanticZoom.

ContentControl does not change the content - function never called

I want to change my content off an AppBar dynamicly whith this code:
<Page.Resources>
<local:AppBarSelector x:Key="myAppBarSelector"/>
</Page.Resources>
<Page.BottomAppBar>
<AppBar>
<ContentControl Content="{Binding SelectedItem, ElementName=listBox}" ContentTemplateSelector="{StaticResource myAppBarSelector}">
<ContentControl.Resources>
<DataTemplate x:Key="1">
<TextBlock Text="Hallo Welt 1" Foreground="White" />
</DataTemplate>
<DataTemplate x:Key="2">
<TextBlock Text="Hallo Welt 2" Foreground="White" />
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</AppBar>
</Page.BottomAppBar>
And this is my Code behind:
public class AppBarSelector : DataTemplateSelector
{
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
Debug.WriteLine((string)item);
if (item == null) return base.SelectTemplateCore(item, container);
var contentControl = (ContentControl)container;
var templateKey = (string)item;
return (DataTemplate)contentControl.Resources[templateKey];
}
}
But this method is nerver called.Even the Debug.WriteLine function. Where is my mistake?
Just after some comments here...
(note: this is a bit general but I can't be more specific w/o some more code to reflect the issues)
This should work 'as is' - I don't see any problems that would produce that (I check with similar example fast and it works well with .ItemsSource = new List<string>{...}.
So that's not the culprit - but it doesn't hurt what I suggested - make a proper MVVM binding to properties, make the list ObservableCollection<> - and also it's always recommended to have a more higher-level objects (instead of just string) as your items (helps in many cases with binding with similar issues - that object implements INotifyPropertyChanged etc. - and you bind to a 'property' there, not the entire object).
The other error suggests some issues as well.
And lastly to bind two contentControls together - you don't normally need events as such. You can use Triggers from the style or XAML directly - but most of the time just bind both to a property in the view-model - and handle the 'change' in your property 'setter'.
You should put up a small primer that repeats this - who knows it might help you realize what you're doing wrong.