Reputation: 5208
I have the following in a WPF project:
Main Window
<Window x:Class="DataTemplateEventTesting.Views.MainWindow"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
...
xmlns:vm="clr-namespace:DataTemplateEventTesting.ViewModels"
xmlns:vw="clr-namespace:DataTemplateEventTesting.Views">
<Window.DataContext>
<vm:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions> ... </Grid.ColumnDefinitions>
<ListView ItemsSource="{Binding SubViewModels}"
SelectedValue="{Binding MainContent, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type vm:SubViewModel}">
<TextBlock Text="{Binding DisplayText}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ContentControl Grid.Column="1" Content="{Binding MainContent}">
<ContentControl.Resources>
<DataTemplate x:Shared="False" DataType="{x:Type vm:SubViewModel}">
<vw:SubView />
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</Grid>
</Window>
SubView (View for SubViewModel)
<UserControl x:Class="DataTemplateEventTesting.Views.SubView"
...
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity">
<Grid>
<ListView ItemsSource="{Binding Models}">
<ListView.View> ... </ListView.View>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<i:InvokeCommandAction CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type ListView}}}"
Command="{Binding PrintCurrentItemsCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ListView>
</Grid>
</UserControl>
The problem is with the SelectionChanged
EventTrigger
in SubView
.
PrintCurrentItemsCommand
accepts a ListView
as a parameter and prints a count of its items by executing the following method:
private void PrintCurrentItems(ListView listView)
{
System.Diagnostics.Debug.WriteLine("{0}: {1} items.", DisplayText, listView.Items.Count);
}
When I navigate from one SubView
(where some items in its ListView
are selected) to another SubView
, the SelectionChanged
event is fired on the ListView
of the first SubView
. This executes the PrintCurrentItemsCommand
on the correct SubViewModel
, but passes the new (incorrect) ListView
as a parameter. (Either that, or the event is being fired by the new ListView
, and the command is using the DataContext
from the old ListView
.)
Thus, while SubViewModel
with DisplayText
of "Sub1" has 2 items in its Models
collection, and "Sub2" has 3 items, I see the following in the Output window:
Sub1: 2 items. // selected an item
Sub1: 3 items. // navigated to Sub2
Sub2: 3 items. // selected an item
Sub2: 2 items. // navigated to Sub1
Sub1: 2 items. // selected an item
Sub1: 3 items. // navigated to Sub2
Sub2: 3 items. // selected an item
Sub2: 2 items. // navigated to Sub1
Obviously the expected behaviour would be that the correct ListView
would be passed.
The main confusion is that, for example, the command for "Sub1" is able to access the ListView
for "Sub2" at all.
I read something about WPF caching templates, and thought I had found the solution in setting x:Shared = "False"
on the DataTemplate
, but this didn't change anything.
Is there an explanation for this behaviour? And is there a way around it?
Upvotes: 1
Views: 1375
Reputation: 5208
It turns out that the problem was caused by the persistence of the DataTemplate
.
As Ed Plunkett observed, it was the same ListView
the whole time, and only the DataContext
was changing. I imagine that what was happening was that the navigation took place, then the event was fired, and by this time the DataContext
had changed - a simple property change.
In the hoped for behaviour, the old ListView
would fire the event, and execute the first ViewModel's command, and this would occur after the navigation, hence, its items would be counted at 0. But with DataTemplate
sharing, the first ListView
is the second ListView
, so its items are not counted at 0, they've been replaced with the items from the second ViewModel. This occurs after the navigation, so it would be expected that the RelativeSource
would return the ListView
with the second ViewModel as its DataContext
.
I have managed to override this default behaviour by using a custom DataTemplateSelector
class:
public class ViewSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (container is FrameworkElement element && item is SubViewModel)
{
return element.FindResource("subviewmodel_template") as DataTemplate;
}
return null;
}
}
The DataTemplate
is stored in a ResourceDictionary
(merged in App.xaml):
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DataTemplateEventTesting.Views"
xmlns:vm="clr-namespace:DataTemplateEventTesting.ViewModels">
<DataTemplate x:Shared="False" x:Key="subviewmodel_template" DataType="{x:Type vm:SubViewModel}">
<local:SubView />
</DataTemplate>
</ResourceDictionary>
It turns out that in a ResourceDictionary
, x:Shared="False"
has the critical effect that I want it to have (apparently this is effective only in a ResourceDictionary
) - it keeps the templates isolated per ViewModel.
The Main Window is now written as:
<Window x:Class="DataTemplateEventTesting.Views.MainWindow"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
...
xmlns:vm="clr-namespace:DataTemplateEventTesting.ViewModels"
xmlns:vw="clr-namespace:DataTemplateEventTesting.Views">
<Window.DataContext>
<vm:MainWindowViewModel />
</Window.DataContext>
<Window.Resources>
<vw:ViewSelector x:Key="view_selector" />
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions> ... </Grid.ColumnDefinitions>
<ListView ItemsSource="{Binding SubViewModels}"
SelectedValue="{Binding MainContent, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate DataType="{x:Type vm:SubViewModel}">
<TextBlock Text="{Binding DisplayText}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ContentControl Grid.Column="1" Content="{Binding MainContent}"
ContentTemplateSelector="{StaticResource view_selector}" />
</Grid>
</Window>
Interestingly, I found that both of the following need to be in place in this particular example:
One
The DataTemplate
is in a ResourceDictionary
with x:Shared="False"
.
Two
The DataTemplateSelector
is used.
E.g., when I satisfy the first condition, and use <ContentControl ... ContentTemplate="{StaticResource subviewmodel_template}" />
, the issue prevails.
Similarly, when x:Shared="False"
is not present, the DataTemplateSelector
is no longer effective.
Once these two conditions are in place, the Output window shows me:
Sub1: 2 items. // selected an item
Sub1: 0 items. // navigated to Sub2
Sub2: 3 items. // selected an item
Sub2: 0 items. // navigated to Sub1
Sub1: 2 items. // selected an item
Sub1: 0 items. // navigated to Sub2
Sub2: 3 items. // selected an item
Sub2: 0 items. // navigated to Sub1
This is the expected behaviour, which I'd previously observed when switching between ViewModels of different types.
Why DataTemplateSelector?
After reading the documentation for x:Shared, I have at least a theory on why DataTemplateSelector
seems to be required for this to work.
As stated in that documentation:
In WPF, the default
x:Shared
condition for resources istrue
. This condition means that any given resource request always returns the same instance.
The key word here would be request.
Without using a DataTemplateSelector
, WPF has certainty on which resource it needs to use. Therefore, it only needs to fetch it once - one request.
With a DataTemplateSelector
, there is no certainty, as there may be further logic within the DataTemplateSelector
even for ViewModels of the same type. Therefore, DataTemplateSelector
forces a request to be made with each change in Content
, and with x:Shared="False"
, the ResourceDictionary
will always return a new instance.
Upvotes: 2
Reputation: 37059
I was able to reproduce the behavior you're seeing: I select an item in the right hand listview, and then change the selection the left hand listview. When the command is invoked, inside the Execute method, ! Object.ReferenceEquals(this, listView.DataContext)
. I would have expected them to be equal.
With this binding for Command
, they were still not equal:
<i:InvokeCommandAction
Command="{Binding DataContext.PrintCurrentItemsCommand, RelativeSource={RelativeSource AncestorType={x:Type ListView}}}"
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type ListView}}}"
/>
I didn't expect much from that experiment, but it didn't take very long to try.
Unfortunately I don't have time at the moment to investigate this in depth. I haven't been able to find the source code for System.Windows.Interactivity.InvokeCommandAction
, but it certainly looks as if somewhere in the flurry of events and updates accompanying the change, things happen in the wrong order.
The following code is almost unbearably ugly, but it behaves as expected. You could make it less ugly by writing your own behavior. It wouldn't need to be as gloriously generalized as InvokeCommandAction
. Being less generalized, it would be less likely to misbehave the same way, and even if it did, you've got the source and can debug it properly.
SubView.xaml
<ListView
ItemsSource="{Binding Models}"
SelectionChanged="ListView_SelectionChanged"
>
<!-- snip -->
SubView.xaml.ds
private void ListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listView = sender as ListView;
var cmd = listView.DataContext?.GetType().GetProperty("PrintCurrentItemsCommand")?.
GetValue(listView.DataContext) as ICommand;
if (cmd?.CanExecute(listView) ?? false)
{
cmd.Execute(listView);
}
}
Slightly off topic, this would be preferable:
protected void PrintCurrentItems(System.Collections.IEnumerable items)
{
//...
XAML
<i:InvokeCommandAction
Command="{Binding PrintCurrentItemsCommand}"
CommandParameter="{Binding Items, RelativeSource={RelativeSource AncestorType={x:Type ListView}}}"
/>
Code behind
if (cmd?.CanExecute(listView) ?? false)
{
cmd.Execute(listView.Items);
}
The reasoning is that a command that takes IEnumerable
as a parameter is vastly more generally useful than one that expects any collection of items to be packaged in a listview. It's easy to get a collection of items from a listview; it's a real pain to need to have a listview around before you can pass somebody a collection of items. Always accept the least specific parameter you can without shooting yourself in the foot.
And from an MVVM standpoint, it's considered very bad practice for a viewmodel to have any concrete knowledge of UI. What if the UI design team decides later on that it should use a DataGrid or a ListBox instead of a ListView? If they're passing you Items
, it's a total non-issue. If they're passing you ListView
, they have to shoot you an email asking you to change your parameter type, then coordinate with you about it, then there's extra testing, etc. And all to accommodate a parameter that didn't actually need to be ListView
at all.
Upvotes: 2