Reputation: 12610
I'm trying to bind a grouped collection of data items to a DataGrid. The details of the presented data are not relevant, in fact all the contents are set up with dummy data for now.
I followed the sample code found in Microsoft's Sample App and "How to: Group, sort and filter data in the DataGrid Control".
After launching the app the shown DataGrid is empty and the debug output from the binding code says:
Error: Converter failed to convert value of type 'Windows.UI.Xaml.Data.ICollectionView' to type 'IBindableIterable'; BindingExpression: Path='MyContents' DataItem='MyViewModel'; target element is 'Microsoft.Toolkit.Uwp.UI.Controls.DataGrid' (Name='null'); target property is 'ItemsSource' (type 'IBindableIterable').
This is the interesting part of my XAML:
<mstkcontrols:DataGrid ItemsSource="{Binding MyContents}">
<!-- Irrelevant stuff left out... -->
</mstkcontrols:DataGrid>
In my view model I have this code:
public ICollectionView MyContents { get; private set; }
public override void OnNavigatedTo(NavigationEventArgs e)
{
// Irrelevant stuff left out...
ObservableCollection<ObservableCollection<MyItemType>> groupedCollection = new ObservableCollection<ObservableCollection<MyItemType>>();
// It doesn't matter how this grouped collection is filled...
CollectionViewSource collectionViewSource = new CollectionViewSource();
collectionViewSource.IsSourceGrouped = true;
collectionViewSource.Source = groupedCollection;
MyContents = collectionViewSource.View;
}
Is there a conversion from ICollectionView
to IBindableIterable
? If so, how is it done?
I'm well aware that the examples do the binding in the code, not in the XAML. Does this really make a difference?
If this approach is wrong, how is the correct approach?
Edit:
I'm sorry, I forgot to mention that we use the "MVVM Light Toolkit" by GalaSoft. That's why the code to build the collection is in the view model, not the code behind. And it should stay there.
This has an impact on the kind of binding. To bind to a property of the view model, we use:
<mstkcontrols:DataGrid ItemsSource="{Binding MyContents}">
But to bind to a property of the code behind, is has to be:
<mstkcontrols:DataGrid ItemsSource="{x:Bind MyContents}">
In the meantime, thank you very much to all reading and making suggestions. I'm currently investigating how to connect view model and code behind.
Upvotes: 1
Views: 2826
Reputation: 12610
Alright, it took me a 2-digit number of hours to find the root of this problem. There seems to be a disrupted way with Binding
compared to x:Bind
.
"{Binding}
assumes, by default, that you're binding to the DataContext of your markup page." says the documentation "Data binding in depth". And the data context of my page is the view model.
"{x:Bind}
does not use the DataContext as a default source—instead, it uses the page or user control itself." says the documentation "{x:Bind} markup extension". Well, and the compile-time generated code has no problems with the different data types.
The XAML is changed to (the Mode
is important, because the default is OneTime
):
<mstkcontrols:DataGrid ItemsSource="{x:Bind MyContents, Mode=OneWay}" Loaded="DataGrid_Loaded">
<!-- Irrelevant stuff left out... -->
</mstkcontrols:DataGrid>
The code behind needs a property that sends notification events. For this its class needs to inherit from INotifyPropertyChanged
. You could use the methods Set()
and OnPropertyChanged()
shown in @NicoZhu's answer, but this cut-out shows more clearly what is important:
private ICollectionView _myContents;
public ICollectionView MyContents
{
get
{
return _myContents;
}
set
{
if (_myContents != value)
{
_myContents = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MyContents)));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void DataGrid_Loaded(object sender, RoutedEventArgs e)
{
if ((sender as DataGrid).DataContext is MyViewModel viewModel)
{
MyContents = viewModel.ContentsView();
}
}
The view model provides the contents view (as a collection of collections) through a method that is called from the code behind. This method is almost identical to the code I used before.
internal ICollectionView ContentsView()
{
ObservableCollection<ObservableCollection<MyItemType>> groupedCollection = new ObservableCollection<ObservableCollection<MyItemType>>();
// It doesn't matter how this grouped collection is filled...
CollectionViewSource collectionViewSource = new CollectionViewSource();
collectionViewSource.IsSourceGrouped = true;
collectionViewSource.Source = groupedCollection;
return collectionViewSource.View;
}
Upvotes: 4
Reputation: 32775
I follow this tutorial creating a simple sample to reproduce your issue, And binding CollectionViewSource
works well. Please refer the following code. This is sample project.
Xaml
<controls:DataGrid
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
AlternatingRowBackground="Transparent"
AlternatingRowForeground="Gray"
AreRowDetailsFrozen="False"
AreRowGroupHeadersFrozen="True"
AutoGenerateColumns="False"
CanUserReorderColumns="True"
CanUserResizeColumns="True"
CanUserSortColumns="False"
ColumnHeaderHeight="32"
FrozenColumnCount="0"
GridLinesVisibility="None"
HeadersVisibility="Column"
HorizontalScrollBarVisibility="Visible"
IsReadOnly="False"
ItemsSource="{x:Bind GroupView, Mode=TwoWay}"
Loaded="DataGrid_Loaded"
MaxColumnWidth="400"
RowDetailsVisibilityMode="Collapsed"
RowGroupHeaderPropertyNameAlternative="Range"
SelectionMode="Extended"
VerticalScrollBarVisibility="Visible"
>
<controls:DataGrid.RowGroupHeaderStyles>
<Style TargetType="controls:DataGridRowGroupHeader">
<Setter Property="Background" Value="LightGray" />
</Style>
</controls:DataGrid.RowGroupHeaderStyles>
<controls:DataGrid.Columns>
<controls:DataGridTextColumn
Binding="{Binding Name}"
Header="Rank"
Tag="Rank"
/>
<controls:DataGridComboBoxColumn
Binding="{Binding Complete}"
Header="Mountain"
Tag="Mountain"
/>
</controls:DataGrid.Columns>
</controls:DataGrid>
Code Behind
public sealed partial class MainPage : Page, INotifyPropertyChanged
{
public ObservableCollection<Item> MyClasses { get; set; } = new ObservableCollection<Item>();
private ICollectionView _groupView;
public ICollectionView GroupView
{
get
{
return _groupView;
}
set
{
Set(ref _groupView, value);
}
}
public MainPage()
{
this.InitializeComponent();
MyClasses.Add(new Item { Name = "Nico", Complete = false });
MyClasses.Add(new Item { Name = "LIU", Complete = true });
MyClasses.Add(new Item { Name = "He", Complete = true });
MyClasses.Add(new Item { Name = "Wei", Complete = false });
MyClasses.Add(new Item { Name = "Dong", Complete = true });
MyClasses.Add(new Item { Name = "Ming", Complete = false });
}
private void DataGrid_Loaded(object sender, RoutedEventArgs e)
{
var groups = from c in MyClasses
group c by c.Complete;
var cvs = new CollectionViewSource();
cvs.Source = groups;
cvs.IsSourceGrouped = true;
var datagrid = sender as DataGrid;
GroupView = cvs.View;
}
public event PropertyChangedEventHandler PropertyChanged;
private void Set<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
{
if (Equals(storage, value))
{
return;
}
storage = value;
OnPropertyChanged(propertyName);
}
private void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Upvotes: 2
Reputation: 35
I don't know how transitive WPF C# is to UWP, but this is how I do my observable collection data binding in WPF
In my window's .cs:
public partial class MainWindowView : Window, INotifyPropertyChanged
{
public MainWindowView()
{
InitializeComponent();
this.data.ItemsSource = etc;
}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Stuff_NThings> etc = new ObservableCollection<Stuff_NThings>();
private void Button_Click(object sender, RoutedEventArgs e)
{
Stuff_NThings t = new Stuff_NThings();
t.stuff = 45;
t.moreStuff = 44;
t.things = 33;
t.moreThings = 89;
etc.Add(t);
}
My class:
public class Stuff_NThings : INotifyPropertyChanged
{
private int _things;
private int _moreThings;
private int _stuff;
private int _moreStuff;
public int things
{
get
{
return _things;
}
set
{
_things = value;
NotifyPropertyChanged(nameof(things));
}
}
public int moreThings
{
get
{
return _moreThings;
}
set
{
_moreThings = value;
NotifyPropertyChanged(nameof(moreThings));
}
}
public int stuff
{
get
{
return _stuff;
}
set
{
_stuff = value;
NotifyPropertyChanged(nameof(stuff));
}
}
public int moreStuff
{
get
{
return _moreStuff;
}
set
{
_moreStuff = value;
NotifyPropertyChanged(nameof(moreStuff));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
By setting the dataGrid's item source in the mainWindow constructor, it will automatically create the headers in the dataGrid based on the class variable names. Whenever you add an instance of Stuff'NThings (via button, other, whatever, and etc) to the observable collection, the trigger is thrown and it updates the UI. Hope some of this actually applies!
Upvotes: 0