Multiple DataContext bindings in UWP Page and CommandBar - c#

I need to bind some specific elements in my UWP page to a different VIewModel than what the page has.
So in my page I bind to one DataContext:
<Page
DataContext="{Binding RootViewModel, Source={StaticResource Locator}}">
...
and I am now trying to bind the bottomAppBar CommandBar to a different ViewModel:
<Page.BottomAppBar>
<CommandBar DataContext="{Binding OtherViewModel, Source={StaticResource Locator}}">
<CommandBar.Content>
<TextBlock Text="{Binding Status}"></TextBlock>
But it doesn't work, the binding does not occur in the CommandBar.
Why?

By design, the DataContext of the CommandBar doesn't get passed to its Content. Actually, only the DataContext property of a child element will automatically get the same DataContext of its parent, unless you specifically break the flow by assigning another value to it.
In your case, the DataContext of your CommandBar will be OtherViewModel, but the Content property will remain null.
So instead of manually setting the DataContext, you should set its Content instead.
<CommandBar Content="{Binding OtherViewModel, Source={StaticResource Locator}}">
<CommandBar.ContentTemplate>
<DataTemplate>
<TextBlock Text="{Binding Status}"></TextBlock>
</DataTemplate>
</CommandBar.ContentTemplate>
...
</CommandBar>

Related

How to bind to a source inside a ListBox different from the ItemsSource already specified

I have a ListBox inside a HubSection, whose Items are bound to a class "players" added to my DefaulViewModel via code behind.
First I simply put a TextBox bound to the property "PlayerName" of my class "players".
Now I would like to add a ComboBox with some items that are NOT part of the class players.
Is it possible ? I thought that definind an ItemsSource in the ComboBox would sort of override the ItemsSource of the ListBox, but nothing displays.
The DataContext of the whole page is defined like so:
DataContext="{Binding DefaultViewModel, RelativeSource={RelativeSource Self}}"
Then the HubSection is like so:
<HubSection x:Name="HubSec1">
<DataTemplate>
<ListBox x:Name="ListBox1" ItemsSource="{Binding players}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBox Text="{Binding Path=PlayerName, Mode=TwoWay}"/>
<ComboBox ItemsSource="{Binding Path=ListOfElements}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</HubSection>
If I define the ComboBox in the same way but outside the ListBox, it will display the string elements of "ListOfElements" properly.
But in this ListBox, the ComboBox is empty. So my guess is that having defined an ItemsSource for the ListBox, it is not possible to override it.
I have tried to define a DataTemplate but was not successful doing so, but it might be the good solution (and I did not proceed properly)
What am I missing ?
Edit :
The ComboBox items is an ObservableCollection. It is not part of the "players" class.
Here is how I added these elements to the DefaultViewModel
DefaultViewModel.Add("players", players);
DefaultViewModel.Add("MyItemsList", ListOfElements);
You can walk up the visual tree and bind to an ancestors datacontext:
{Binding Path=PathToProperty, RelativeSource={RelativeSource AncestorType={x:Type typeOfAncestor}}}
EX:
{Binding Path=ListOfItems, RelativeSource={RelativeSource AncestorType={x:Type ListBox}}}
that should give you the datacontext that the listbox has, so assuming your ListOfItems exists in that data context.
Or you can name your control, and then bind to its datacontext by element name:
{Binding ElementName=mySourceElement,Path=ListOfItems}
It can be a little bit tricky to create a good working binding in Windows Apps. A widely used work around is to use the Tag property.
<ListBox x:Name="ListBox1" ItemsSource="{Binding players}" Margin="0,184,0,0" Tag="{Binding Path=ListOfElements}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBox Text="{Binding Path=PlayerName, Mode=TwoWay}"/>
<TextBox Text="{Binding Path=Tag, ElementName=ListBox1}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
A binding to an element wirh the specific name will work always. And the ListOfElements should be in the scope of the ListBox so you can use the Tag property as a proxy. If you need to bind more than one property, you can also use dummy XAML elements:
<Border Tag="{Binding ...}" Name="dummy1"/>

How to get property from the DataTemplate's parent ViewModel class

I am using MVVVM Light, so I set the DataContext of the Page to the Locator. Then I set the Pivot's ItemSource to a collection property inside "myFirstVM" ViewModel class. But how to set the text of the header of the PivotItem which is in a dataTemplate of TextBox to "MyProperty" which is also defined in "myFirstVM" class?
I look at this example, but cannot figure it out:
How to access Parent's DataContext in Window 8 store apps
Here is my code:
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:myApp"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ViewModel="using:myApp.ViewModel"
x:Class="myApp.MyTripsPage"
mc:Ignorable="d"
DataContext="{Binding Source={StaticResource Locator}}">
<Grid x:Name="LayoutRoot">
<Pivot Name="myPivot"
Tag="{Binding}"
ItemsSource="{Binding myFirstVM.DataSource}"
ItemTemplate="{Binding myFirstVM.ViewDataTemplate}">
<Pivot.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding MyProperty, ElementName=myPivot}"/>
</DataTemplate>
</Pivot.HeaderTemplate>
</Pivot>
</Grid>
using ElementName in a binding will bind to the element itself (Pivot in this case), whereas you want to bind to something in the DataContext of Pivot, so just add DataContext to your path:
<TextBlock Text="{Binding DataContext.myFirstVM.MyProperty, ElementName=myPivot}"/>

WPF Custom Control Work by itself but not as part of another control

I have a WPF Custom Control that I use to display images.
There is a list view which is bound to an observable collection of database entities, which is subsequently converted into an image by virtue of a value converter.
When I drag the control onto a windows forms project (using a WPF Host control) it works perfectly when assigning the observable collection behind a list. I have tested this and it updates correctly and does everything i need it to.
HOWEVER
I would like to have three such controls displaying related images so I created a second control which simply grouped the three original controls into a stack panel.
I created a method for each that updates the images property.
public void ChangeSearchResults(List<ItemImage> items)
{
SearchResultsImageViewer.ItemImages = new ObservableCollection<ItemImage>(items);
}
However I simply cant get the images to show.
There seems to be a difference between viewing a control directly and viewing a control as a child control.
I am pretty sure it is not the element host in winforms as the control works well by itself.
Is there something I am not realising?
This is the Xaml for the list view
<!-- Sets the template for the data to be displayed -->
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<!-- Defines the actual image being displayed -->
<Image x:Name="ItemImageControl"
Width="100"
Height="200"
Margin="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Cursor="Hand"
Source="{Binding .,
Converter={StaticResource imageConverter}}" />
<TextBlock HorizontalAlignment="Center"
FontWeight="Bold"
Text="{Binding .,
Converter={StaticResource groupNameConverter}}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
EDIT - this is the user control XAML
<UserControl x:Class="Project.CustomControls.ctrlImageCollection"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:CustomControls="clr-namespace:Project.CustomControls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<StackPanel>
<CustomControls:ctrlImageViewer x:Name="ShortlistImageViewer" />
<CustomControls:ctrlImageViewer x:Name="SearchResultsImageViewer" />
<CustomControls:ctrlImageViewer x:Name="GroupImageViewer" />
</StackPanel>
</UserControl>
With using
...
Source={Binding ., Converter={StaticResource imageConverter}}" />
you bind to the current UserControl. When you use your ListView alone, it is bound to the UserControl containing the ListView and will work.
If you put your ListView UserControl into another UserControl its values get bound to its parent UserControl, not longer "own" UserControl.
Try this:
Go to your ListView UserControl xaml.cs and set the DataContext to itself.
//...
DataContext = this;
//...

Navigate properly between tab in a WPF MVVM application

I'm doing a WPF application with the M-V-VM patern (i'm using galasoft if it's relevant), but I have issues when I navigate through a tabcontrol.
I'm adding tabs on the run. All the binding seems to goes well : inside the tab or in the header of the tab.
I've bind my tabcontrol to a observable list. Through an interface I'm adding several types of viewmodel to this list and the binding seems correct.
My XAML code looks like this :
<Grid>
<Grid.Resources>
<DataTemplate x:Key="itemTemplate">
<TechnicalControls:ItemTab />
</DataTemplate>
</Grid.Resources>
<TabControl Grid.Row="1" x:Name="MainTab" Grid.Column="1"
ItemsSource="{Binding TabViewModels}"
SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}"
ItemContainerStyleSelector="{StaticResource LastItemStyleSelector}"
ItemTemplate="{StaticResource itemTemplate}"
>
<TabControl.Resources>
<DataTemplate DataType="{x:Type VM:JobViewModel}" x:Shared="False" >
<FunctionnalControls:Job />
</DataTemplate>
<DataTemplate DataType="{x:Type VM:ExcelJobViewModel}" x:Shared="False">
<FunctionnalControls:ExcelJob />
</DataTemplate>
<DataTemplate DataType="{x:Type VM:MonitoringViewModel}" x:Shared="False">
<FunctionnalControls:Monitoring />
</DataTemplate>
<DataTemplate DataType="{x:Type VM:ErrorViewModel}" x:Shared="False">
<FunctionnalControls:Error />
</DataTemplate>
</TabControl.Resources>
</TabControl>
</Grid>
For example if I go from a ExcelJob to another ExcelJob usercontrol, the new usercontrol is not load properly but it changes then it works, for exemple, I can go to a ExcelJob to another ExcelJob if only I go through the monitoring.
I've already look to this this but it didn't work for me.
I've also looked at this : it says that we should not used inputs because you can focus them. I've tried to set the IsEnabled property on the users controls to false. I did it when tabs were changing. It didn't work...
The only solution that I can see is to go through another a new usercontrol with no other purpose to be used every time a tab is changed but this is ugly, and I'm pretty sure, Microsoft thought about this and came up with a better solution.
If necessary I can put the code of the view model.
EDIT : Just to clarify, when I click on other tab with the same control, instead of showing me the new usercontrol, it shows me the previous one. In order to see the new one, I have to change to another tab with another usercontrol then come back on the one I want to see.
I've look through debug and the when I click on the other tab It doesn't call the viewmodel
<UserControl x:Class="App.ExcelJob"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
DataContext="{Binding Main.ExcelJobVM, Source={StaticResource Locator }}">
<Grid >
<Label>Futur Excel Job</Label>
<TextBox Width="200" Height="60" Text="{Binding Header}"/>
</Grid>
</UserControl>
So Main returns the Mainviewmodel and Main.ExcelJobVM returns the good viewmodel of the usercontrol. the returned isntance is based on selected Index.
The only thing I need is to force the redrawing of the usercontrol or recall the method to update the datacontext, by loading the good viewmodel. I tried, I've failed so far. I'm not sure of what I'm doing because I want to use the event SelectionChanged of the tabcontrol but it would be in the code behind, and I don't know if it would still respect the MVVM pattern.
The problem is that you have the DataContext hardcoded in your UserControl, and your UserControl is a Template.
When you switch to a tab that uses the same Template, WPF doesn't bother to redraw the template and only changes the DataContext behind the template. But in your case, you have the DataContext hardcoded in the UserControl, so it's not using the existing data context from the TabItem.
The best solution would be to remove the DataContext binding from your UserControl, and let it be inherited from the TabItem when the selected item changes.
For example:
WPF says
User has selected ExcelJobA for display. Because of the DataTemplate, let me draw it using an ExcelJob UserControl
<TabItem>
<ContentPresenter DataContext="ExcelJobA">
<local:ExcelJob DataContext="{Binding Main.ExcelJobVM, Source={StaticResource Locator }}" />
</ContentPresenter>
</TabItem>
So an ExcelJob UserControl gets created, and by default the DataContext of ExcelJobA would be inherited by the UserControl.
When the user changes the selected tab to ExcelJobB, WPF goes
Hey, the user has changed to ExcelJobB. Because of the DataTemplate, let me draw it using an ExcelJob UserControl, BUT WAIT! I'm already displaying an ExcelJob UserControl, so let me just change the DataContext behind that to ExcelJobB
<TabItem>
<ContentPresenter DataContext="ExcelJobB">
<local:ExcelJob DataContext="{Binding Main.ExcelJobVM, Source={StaticResource Locator }}" />
</ContentPresenter>
</TabItem>
So the actual displayed ExcelJob UserControl does not get recreated or redrawn, but only the DataContext behind it changes.
HOWEVER, because you have hard-coded the DataContext inside your UserControl, the actual data context obtained from the selected item doesn't ever get used, because a DataContext specified from inside a <Tag> always takes precedence over a DataContext that would be inherited from further up the visual tree.
You need to remove the DataContext binding from inside your UserControl, and let it get passed in normally from your TabControl's SelectedItem, and it will work fine.
<TabItem>
<ContentPresenter DataContext="ExcelJobA">
<local:ExcelJob /> <!-- DataContext inherited from the ContentPresenter -->
</ContentPresenter>
</TabItem>

WPF: Accessing two DataContexts in the same control

I am using an MVVM approach, and I have an object from my ViewModel called DatabasesSubFrame which is DataTemplated to show a ListBox. I want to display a Button below the ListBox, which binds to both the currently SelectedItem, and a property on the DatabasesSubFrame object which is being DataTemplated.
I know how to refer to the currently selected item, by setting the DataContext on a shared ancestor with the ListBox and use {Binding /}. In this example the shared ancestor is a StackPanel. And if the DataContext wasn't explicitly set there I could easily bind to a property on the DatabasesSubFrame object by just doing {Binding SomeProperty}. However, if I do {Binding SomeProperty} within the explicitly set DataContext, it refers to the wrong DataContext.
How do I access the "original" DataContext here? I tried messing with RelativeSources and TemplatedParents but couldn't figure out how to fit them in.
<DataTemplate DataType="{x:Type VM:DatabasesSubFrame}">
<StackPanel DataContext="{Binding Databases}" >
<ListBox Name="DbInfoBox"
ItemsSource="{Binding}"
IsSynchronizedWithCurrentItem="True">
<ListBox.ItemTemplate>
<DataTemplate>
<Label Content="{Binding ShortName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Problem: The Command and V:CreateCommandBinding.Command are set incorrectly here. How do I access OpenDbCommand from the top-level DataTemplate's DataContext? -->
<Button Content="Open Database"
CommandParameter="{Binding /}"
Command="{Binding ???, Path=OpenDbCommand.Command}"
V:CreateCommandBinding.Command="{Binding ???, Path=DataContext.OpenDbCommand}"/>
</StackPanel>
</DataTemplate>
I think this question will help you to find the answer to yours. Another trick is to set the Name of the Window to something like "Root". You can then get at the window's original datacontext by using:
{Binding ElementName=Root, Path=DataContext.MyViewModelsProperty}

Categories