Alberto Rivelli Alberto Rivelli - 1 month ago 20
C# Question

ComboBox binding to grouped collection

I'm facing a very difficult problem with binding. The app attached is extremely simple as you can see and is a trimmed down reproduction of the original code in which the bug appears.
To reproduce the problem, launch the app and press the 3 buttons with the + icon starting from left to right. These buttons will add 3 items in the collection. Then press the 4th button to navigate to the second page. On the second page select the TEA element from the combobox. Go back to the main page and press the last button on the right which adds the CAPPUCCINO product to the list. You will get an

Value does not fall within expected range
exception. I would like to know why this happens and not just a workaround which could not be the real solution to the problem. As you can see the problem appears under very specific circumstances.
Notes:


  • removing the static variable and passing the collection instance in the
    Navigate
    call to the second page does not fix the problem

  • in place of the
    Add
    call there was an insertion in order which called
    Insert
    on the collection. I removed it and the
    Insert
    is not the problem nor the solution

  • removing the
    CollectionViewSource
    on the second page does not fix the problem

  • creating another specular collection
    GroupByLetter2
    for the combobox does not fix the problem (see test1 branch)



UPDATE: at the end I was able to set the
CollectionViewSource.Source = null
on the
UserControl.Unloaded
event and that fixed it. But the question is still open on the working theory.



https://github.com/albertorivelli/app1

This is the main page:

<Page.Resources>
<CollectionViewSource x:Name="cvsProductsLetter" IsSourceGrouped="true" />
</Page.Resources>

<ListView x:Name="gwProducts" ItemsSource="{Binding Source={StaticResource cvsProductsLetter}}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" FontSize="20" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>


The
CollectionViewSource
it's populated in code-behind:

public sealed partial class MainPage : Page
{
public static ProductCollection _productcollection;

public MainPage()
{
this.InitializeComponent();

NavigationCacheMode = NavigationCacheMode.Enabled;

_productcollection = new App1.ProductCollection();

cvsProductsLetter.Source = _productcollection.GroupByLetter;
}


This is the
ProductCollection
class:

public class ProductCollection
{
public ObservableCollection<ProductGroup> _groupsletter;

public ObservableCollection<ProductGroup> GroupByLetter
{
get
{
if (_groupsletter == null)
{
_groupsletter = new ObservableCollection<ProductGroup>();
}

return _groupsletter;
}
}

public void Add(Product newitem)
{
AddToLetterGroup(newitem);
}

private void AddToLetterGroup(Product item)
{
int i;
ProductGroup prodgr = null;

// get group from letter
for(i = 0; i < _groupsletter.Count; i++)
{
if (String.Equals(_groupsletter[i].Key, item.Name[0].ToString(), StringComparison.CurrentCultureIgnoreCase))
{
prodgr = _groupsletter[i];
break;
}
}

//new letter
if (prodgr == null)
{
prodgr = new ProductGroup();
prodgr.Key = item.Name[0].ToString();
prodgr.Add(item);
_groupsletter.Add(prodgr);
}
else
{
prodgr.Add(item);
}
}
}

public class ProductGroup : ObservableCollection<Product>
{
public string Key { get; set; }
}


..and the
Product
class

public class Product : INotifyPropertyChanged
{
private string _name = "";

public event PropertyChangedEventHandler PropertyChanged;

public string Name
{
get { return _name; }
set
{
if (!String.Equals(_name, value))
{
_name = value;
OnPropertyChanged("Name");
}
}
}

protected void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

public override string ToString()
{
return Name;
}
}


This is the second page with the combobox:

<Page.Resources>
<CollectionViewSource x:Name="cvsProductsLetter" IsSourceGrouped="true" />
</Page.Resources>

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ComboBox ItemsSource="{Binding Source={StaticResource cvsProductsLetter}}" FontSize="20" Foreground="Black" />
</Grid>

public sealed partial class SecondPage : Page
{
public SecondPage()
{
this.InitializeComponent();
cvsProductsLetter.Source = App1.MainPage._productcollection.GroupByLetter;
}

Answer

Go back to the main page and press the last button on the right which adds the CAPPUCCINO product to the list. You will get an Value does not fall within expected range exception.

I can reproduce this problem in your demo. I did some test and found that on the second Page when you select TEA, then go back to the MainPage only the T group can be added without exception. And when you select CAPPUCCINO and repeat the steps, C group won't get exception, others will.

I guess it is due to you are sharing the same data model object between pages. For detailed root cause, I need to consult internally.

Currently the simpliest workaround is to empty the cvsProductsLetter in the OnNavigatingFrom of Second Page:

SecondPage.xaml.cs:

protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
    cvsProductsLetter.Source = null;
}

But the recommended way to solve this problem is to create a separated new data model for the second Page:

ProductCollection.cs:

public class ProductCollection
{
  ...
    public void CreateNewGroupByLetter(ObservableCollection<ProductGroup> oldGroupByLetter)
    {
        if (oldGroupByLetter != null&&this.GroupByLetter!=null)//add this.GroupByLetter!=null to call the setter of GroupByLetter.
        {
            foreach (var group in oldGroupByLetter)
            {
                foreach (var product in group)
                {
                    Add(new Product {
                         Name=product.Name
                    });
                }
            }
        }
    }
}

And MainPage.xaml.cs:

private void btnProdNavigate_Click(object sender, RoutedEventArgs e)
{
    ProductCollection newColl = new ProductCollection();
    newColl.CreateNewGroupByLetter(_productcollection.GroupByLetter);
    this.Frame.Navigate(typeof(SecondPage), newColl);
}

Update:

Here is the link to the modified demo: App1.

Comments