here is what i want: if i bind a ICollectionview to a DataGrid, i dont wanna loose the SortDescription in my Viewmodel.
i create a small sample project to see what i mean. In my projects i simply use a Usercontrol to show my data in a DataGrid. If i do this the SortDescritpion is gone when the UserControl Unload, because the ItemsSource is set to null. If i use a TemplateSelector to show my UserControl, the SortDescription is not gone and the ItemsSource ist not set to null on Unload. the question is, why are these different behaviors? Is one on the 2 behaviors a bug?
btw. I use .Net 4.5.1 but 4.6.1 is installed and system.Windows.Interactivity 4.0.0.0
MainWindow.xaml
<Window x:Class="DataGridICollectionView.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DataGridICollectionView"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type local:ViewmodelListe}">
<local:MyViewUc/>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ToolBar Grid.Row="0">
<Button Content="SetWorkspace MyView" Click="Button_Click"/>
<Button Content="SetWorkspace Other" Click="Button_Click_1"/>
</ToolBar>
<ContentPresenter Grid.Row="1" Content="{Binding Workspace}"/>
</Grid>
</Window>
MainWindow.xaml.cs
namespace DataGridICollectionView
{
/// <summary>
/// Interaktionslogik für MainWindow.xaml
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged
{
private object _workspace;
public MainWindow()
{
InitializeComponent();
MyViewVm = new ViewmodelListe();
DataContext = this;
}
public ViewmodelListe MyViewVm { get; set; }
public object Workspace
{
get { return _workspace; }
set
{
_workspace = value;
OnPropertyChanged();
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Workspace = MyViewVm;
}
private void Button_Click_1(object sender, RoutedEventArgs e)
{
Workspace = "Other";
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class ViewmodelListe : INotifyPropertyChanged
{
public ViewmodelListe()
{
Persons = new ObservableCollection<Person>();
MyView = CollectionViewSource.GetDefaultView(Persons);
Persons.Add(new Person() {FirstName = "P1", LastName = "L1"});
Persons.Add(new Person() {FirstName = "P2", LastName = "L2"});
Persons.Add(new Person() {FirstName = "P3", LastName = "L3"});
}
public ObservableCollection<Person> Persons { get; private set; }
public ICollectionView MyView { get; private set; }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Person : INotifyPropertyChanged
{
private string _firstName;
private string _lastName;
public string FirstName
{
get { return _firstName; }
set
{
_firstName = value;
OnPropertyChanged();
}
}
public string LastName
{
get { return _lastName; }
set
{
_lastName = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class TestBehavior : Behavior<DataGrid>
{
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Unloaded += AssociatedObjectUnloaded;
}
private void AssociatedObjectUnloaded(object sender, RoutedEventArgs e)
{
//look at this in Debug Mode, its NULL if you dont use the TemplateSelector
var itemssource = AssociatedObject.ItemsSource;
}
protected override void OnDetaching()
{
base.OnDetaching();
AssociatedObject.Unloaded -= AssociatedObjectUnloaded;
}
}
}
MyGridControl.xaml
<UserControl x:Class="DataGridICollectionView.MyGridControl"
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"
xmlns:local="clr-namespace:DataGridICollectionView"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<DataGrid ItemsSource="{Binding MyView}" AutoGenerateColumns="True">
<i:Interaction.Behaviors>
<local:TestBehavior/>
</i:Interaction.Behaviors>
</DataGrid>
</Grid>
</UserControl>
MyViewUc.xaml
<UserControl x:Class="DataGridICollectionView.MyViewUc"
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"
xmlns:local="clr-namespace:DataGridICollectionView"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<DataTemplate x:Key="MyViewCrap">
<local:MyGridControl/>
</DataTemplate>
<local:MyTemplateSelector x:Key="Selector" GridView="{StaticResource MyViewCrap}" />
</UserControl.Resources>
<Grid>
<!--When using Contentcontrol with TemplateSelector- ItemsSource is NOT set to null -->
<ContentControl Content="{Binding .}" ContentTemplateSelector="{StaticResource Selector}"/>
<!--When using MyGridControl withOUT TemplateSelector- ItemsSource is set to NULL -->
<!--<local:MyGridControl/>-->
</Grid>
</UserControl>
MyViewUc.xaml.cs
namespace DataGridICollectionView
{
/// <summary>
/// Interaktionslogik für MyViewUc.xaml
/// </summary>
public partial class MyViewUc : UserControl
{
public MyViewUc()
{
InitializeComponent();
}
}
public class MyTemplateSelector : DataTemplateSelector
{
public DataTemplate GridView { get; set; }
public override DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
var chooser = item as ViewmodelListe;
if (chooser == null)
{
return base.SelectTemplate(item, container);
}
return GridView;
}
}
}
EDIT: i end up using this
public class MyDataGrid : DataGrid
{
static MyDataGrid ()
{
ItemsSourceProperty.OverrideMetadata(typeof(MyDataGrid ),new FrameworkPropertyMetadata(null, OnPropertyChangedCallBack, OnCoerceItemsSourceProperty));
}
private ICollectionView _defaultView;
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if(_defaultView != null)
_defaultView.CollectionChanged -= LiveSortingPropertiesOnCollectionChanged;
base.OnItemsSourceChanged(oldValue, newValue);
_defaultView = newValue as ICollectionView;
if(_defaultView != null)
_defaultView.CollectionChanged += LiveSortingPropertiesOnCollectionChanged;
}
private void LiveSortingPropertiesOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
{
foreach (var dataGridColumn in this.Columns)
{
var isSortDirectionSetFromCollectionView = false;
foreach (var sortDescription in _defaultView.SortDescriptions)
{
if (dataGridColumn.SortMemberPath == sortDescription.PropertyName)
{
dataGridColumn.SortDirection = sortDescription.Direction;
isSortDirectionSetFromCollectionView = true;
break;
}
}
if (!isSortDirectionSetFromCollectionView)
{
dataGridColumn.SortDirection = null;
}
}
}
}
private static void OnPropertyChangedCallBack(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var grd = d as MyDataGrid ;
var view = e.NewValue as ICollectionView;
if (grd == null || view == null)
return;
foreach (var dataGridColumn in grd.Columns)
{
var isSortDirectionSetFromCollectionView = false;
foreach (var sortDescription in view.SortDescriptions)
{
if (dataGridColumn.SortMemberPath == sortDescription.PropertyName)
{
dataGridColumn.SortDirection = sortDescription.Direction;
isSortDirectionSetFromCollectionView = true;
break;
}
}
//wenn die View nicht sortiert war, auch die column nicht Sortieren
if (!isSortDirectionSetFromCollectionView)
{
dataGridColumn.SortDirection = null;
}
}
}
private static object OnCoerceItemsSourceProperty(DependencyObject d, object baseValue)
{
// do nothing here - we just want to override parent behaviour.
// The _only_ thing DataGrid does here is clearing sort descriptors
return baseValue;
}
}
When you host your MyGridControl directly inside MyViewUc (case 1) - when you switch workspace and MyViewUC is unloaded, it's datacontext is set to null. Because MyGridControl is direct child - it's datacontext is set to null too, and in turn DataContext of DataGrid. This sets ItemsSource to null too, because it's bound to DataContext. You can verify this by looking at DataContext of DataGrid in your behavior. This behavior is completely reasonable to my mind.
When you use template selector: MyViewUC is unloaded, it's datacontext is set to null. Then ContentControl Content is set to null, too. Now here is the problem: when you use ContentTemplateSelector, DataContext of your old (unloaded) MyGridControl is NOT set to null. You can verify this in your behaviour, that is why ItemsSource and sort descriptors are preserved.
Now, I believe this second behaviour is not correct and datacontext should be set to null for this unloaded control created by ContentTemplateSelector. The logic behind this is not very simple - you can look yourself at source code of ContentPresenter.OnContentChanged method, where you will see when DataContext is not updated when content changes.
UPDATE: I see your main concern is losing sort descriptors, but that is direct consequence of losing DataContext and setting ItemsSource to null. To me this behaviour looks reasonable, but I see indeed for many people it is not, so that there is even bug report about this issue: https://connect.microsoft.com/VisualStudio/feedback/details/592897/collectionviewsource-sorting-only-the-first-time-it-is-bound-to-a-source
You can see yourself in DataGrid source code that:
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
base.OnItemsSourceChanged(oldValue, newValue);
if (newValue == null)
this.ClearSortDescriptionsOnItemsSourceChange();
// more code here....
}
So when you set ItemsSource to null - all sort descriptors are explicitly cleared. At link above you can find some workarounds which you might find useful.
UPDATE2: you can consider trying to fix that behaviour by inheriting from DataGrid. I don't say that is perfect solution, but neither is using ContentTemplateSelector. There are two places where sort descriptors are cleared when ItemsSource is set to null - in OnItemsSourceChanged and OnCoerceItemsSourceProperty. So you can do the following:
public class MyDataGrid : DataGrid {
static MyDataGrid() {
ItemsSourceProperty.OverrideMetadata(typeof(MyDataGrid), new FrameworkPropertyMetadata(null, OnCoerceItemsSourceProperty));
}
private static object OnCoerceItemsSourceProperty(DependencyObject d, object baseValue) {
// do nothing here - we just want to override parent behaviour.
// The _only_ thing DataGrid does here is clearing sort descriptors
return baseValue;
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue) {
SortDescription[] sorts = null;
if (newValue == null) {
// preserve sort descriptors when setting ItemsSource to null
sorts = Items.SortDescriptions.ToArray();
}
// they will now be cleared here
base.OnItemsSourceChanged(oldValue, newValue);
if (sorts != null) {
// restore them back
foreach (var sort in sorts) {
Items.SortDescriptions.Add(sort);
}
}
}
}
With code above you will see that sort descriptors are preserved in your MyView between switching datacontext.