the busybee
the busybee

Reputation: 12610

UWP Toolkit DataGrid - How to bind CollectionViewSource to ItemsSource of DataGrid?

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

Answers (3)

the busybee
the busybee

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

Nico Zhu
Nico Zhu

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

Bbylsma
Bbylsma

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! Updated UI for the button click

Upvotes: 0

Related Questions