1

I'm having an enormous amount of trouble with ComboBoxes in a data grid. And really would like some help, i think i've gotten confused by the amount of research and things i've tried. This really should be simple so i must be missing something.

SIMPLIFIED PROBLEM

I use a CollectionViewSource in xaml, the C# sets the source of that CollectionViewSource to an ObservableCollection in a class that is the Page's datacontext. Adding items to the collection does not update the DataGridComboBox column containing that displays the view source. See below the line for more detail


OVERVIEW

I have a WPF Page with a datagrid on it. The page has its data context set to a view model. The viewModel contains two observable collections. One for Equips and One for Locations. Each Equip has a Location. These are populated from a code first EF database but i believe that this problem is above that level.

The datagrid is one row per Equip. The Location column needs to be a selectable combobox that allows the user to change Location.

The only way i could get the location combobox to populate at all is by binding it to a separate collection view source.

PROBLEM

It seems that if the Page loaded event occurs prior to the ViewModel populating the ObservableCollection then the locationVwSrc will be empty and the property changed event doesn't get this to change.

IMPLEMENTATION SHORT VERSION Page has a collection viewSource defined in the xaml.

Loaded="Page_Loaded"
  Title="EquipRegPage">
<Page.Resources>
    <CollectionViewSource x:Key="locationsVwSrc"/>
</Page.Resources>

The datagrid is defined with xaml.

<DataGrid x:Name="equipsDataGrid" RowDetailsVisibilityMode="VisibleWhenSelected" Margin="10,10,-118,59" 
              ItemsSource="{Binding Equips}" EnableRowVirtualization="True" AutoGenerateColumns="False">

The combobox column defined in xaml

<DataGridComboBoxColumn x:Name="locationColumn" Width="Auto" MaxWidth="200" Header="Location"
                                    ItemsSource="{Binding Source={StaticResource locationsVwSrc}, UpdateSourceTrigger=PropertyChanged}"
                                    DisplayMemberPath="Name"
                                    SelectedValueBinding="{Binding Location}"

The page context set to the view model

public partial class EquipRegPage : Page
{
    EquipRegVm viewModel = new EquipRegVm();

    public EquipRegPage()
    {
        InitializeComponent();
        this.DataContext = viewModel;
    }

Loaded event setting the context

private void Page_Loaded(object sender, RoutedEventArgs e)
    {
        // Locations View Source
        System.Windows.Data.CollectionViewSource locationViewSource =
            ((System.Windows.Data.CollectionViewSource)(this.FindResource("locationsVwSrc")));
        locationViewSource.Source = viewModel.Locations;
        // Above does not work if the viewmodel populates these after this call, only works if its populated prior.
        //TODO inotifypropertychanged not correct? This occurs before the viewmodels loads, and doesn't display.
        // Therefore notify property changes aren't working.

        // Using this as cheat instead instead works, i beleive due to this only setting the source when its full
        //viewModel.Db.Locations.Load();
        //locationViewSource.Source = viewModel.Db.Locations.Local;
        //locationViewSource.View.Refresh();
    }

The ViewModel class and how it loads

public class EquipRegVm : DbWrap, INotifyPropertyChanged
{
    /// <summary>
    /// Event triggered by changes to properties. This notifys the WPF UI above which then 
    /// makes a binding to the UI.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Notify Property Changed Event Trigger
    /// </summary>
    /// <param name="propertyName">Name of the property changed. Must match the binding path of the XAML.</param>
    void RaisePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }


    public ObservableCollection<Equip> Equips { get; set; }

    public ObservableCollection<Location> Locations { get; set; }

    public EquipRegVm() : base()
    {
        Load();
    }

    /// <summary>
    /// Load the data from the Model.
    /// </summary>
    public async void Load()    //TODO async an issue?
    {
        // EQUIPMENT
        ObservableCollection<Equip> eqList = new ObservableCollection<Equip>();
        var eqs = await (from eq in Db.Equips
                        orderby eq.Tag
                        select eq).ToListAsync();
        foreach(var eq in eqs)
        {
            eqList.Add(eq);
        }
        Equips = eqList;
        RaisePropertyChanged("Equips");


        // LOCATIONS
        ObservableCollection<Location> locList = new ObservableCollection<Location>();
        var locs = await (from l in Db.Locations
                         orderby l.Name
                         select l).ToListAsync();
        foreach (var l in locs)
        {
            locList.Add(l);
        }
        Locations = locList;
        RaisePropertyChanged("Locations");
    }
}
Asvaldr
  • 197
  • 3
  • 15

2 Answers2

3

It seems you haven't been able to break the problem down into small enough problems. The question seems to be a mix of ComboBoxes in Datagrid, Asynchronously setting CollectionViewSource source, loading data from a database. I suggest that it would be beneficial to either consider

  1. recreating the problem (or soultion) with the minimum moving parts i.e. a XAML file and a ViewModel with pre canned data.

  2. or decoupling your existing code. It appears that Page knows about your ViewModel explicitly (EquipRegVm viewModel = new EquipRegVm();), and you ViewModel knows explicitly about Databases and how to load itself. Oh snap, now our views are coupled to your database? Isn't that the point of patterns like MVVM so that we are not coupled?

Next I look at some the code and see some more (of what I would call) anti-patterns.

  • Settable collection properties
  • code behind for the page (all could live in the XAML)

But I think basically if you just changed your code in 3 places you should be fine.

Change 1

    /*foreach(var eq in eqs)
    {
        eqList.Add(eq);
    }
    Equips = eqList;
    RaisePropertyChanged("Equips");*/
    foreach(var eq in eqs)
    {
        Equips.Add(eq);
    }

Change 2

    /*foreach (var l in locs)
    {
        locList.Add(l);
    }
    Locations = locList;
    RaisePropertyChanged("Locations");*/
    foreach (var l in locs)
    {
        Locations.Add(l);
    }

Change 3

Either just remove the usage of the CollectionViewSource (what does it offer you?) or use binding to set the source. As you are currently manually setting the Source (i.e. locationViewSource.Source = viewModel.Locations;) you have opted out of getting that value updated when the PropertyChanged event has been raised.

So if you just delete the CollectionViewSource, then you just have to bind to the Locations property. If you decide to keep the CollectionViewSource then I would suggest deleting the page codebhind and just changing the XAML to

<CollectionViewSource x:Key="locationsVwSrc" Source="{Binding Locations}" />
Lee Campbell
  • 10,631
  • 1
  • 34
  • 29
  • Thanks for reply. Can you elaborate more on how to bind to the locations property. I've tried alot of ways to do that and i can never seem to get it correct. The CollectionViewSource was a work around for that. – Asvaldr Oct 08 '16 at 05:42
  • Oh right, well I assume your datacontext on the row is the `Equip` type not the parent `EquipRegVm`. If you want to reach backout to the parent type you will need to use a RelativeSource. So something like `` – Lee Campbell Oct 08 '16 at 05:48
  • As shown in the post code, the DataGrid is bound to "Equips", not the VM. Making the change you suggested returned me to the error of System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.DataGrid', AncestorLevel='1''. BindingExpression:Path=DataContext.Locations; DataItem=null; target element is 'DataGridComboBoxColumn' (HashCode=57273980); target property is 'ItemsSource' (type 'IEnumerable') – Asvaldr Oct 08 '16 at 05:55
  • Sorry just giving generall guidance. As you havn't posted a MVCE, it is hard to be specific. Regardless, in your code you have posted the DataGrid has its ItemSource bound to `Equips`. As the `Equips` property is on the `EquipRegVm` type, this means the DataContext should also have access to the sister property `Location`. In your case maybe stick with the CVS, – Lee Campbell Oct 08 '16 at 06:02
  • Thank you. Your answers have been great to give me more insight in to how far i'm away from MVVM. I've been originally trying to learn the MVVM pattern but as many people say, its enormously cumbersome when firt being introduced. Due to my schedule that i've now burnt up, i'm try to follow MVVM concepts where i can so in the future it can be fixed up if the need requires. – Asvaldr Oct 08 '16 at 06:04
1

Set a Binding like below :

System.Windows.Data.CollectionViewSource locationViewSource =
           ((System.Windows.Data.CollectionViewSource)(this.FindResource("locationsVwSrc")));
// locationViewSource.Source = viewModel.Locations;

Binding b = new Binding("Locations");
b.Source = viewModel;
b.Mode = BindingMode.OneWay;
BindingOperations.SetBinding(locationViewSource, CollectionViewSource.SourceProperty, b);

This is all you need.

AnjumSKhan
  • 9,647
  • 1
  • 26
  • 38
  • This certainly fixed the immediate issue. Thank you @AnjumSKhan. Lee Campbell makes some good points which i'm certainly interested to learn more from him. – Asvaldr Oct 08 '16 at 06:01
  • Yes, this will fix the issue, but by adding even more unnecessary code. – Lee Campbell Oct 08 '16 at 06:04
  • I agree Lee, if you have any more insights i'm keen to learn. Please see my last comment on your solution. – Asvaldr Oct 08 '16 at 06:13