views:

377

answers:

5

Scope of question expanded on 2010-03-25

I ended up figuring out my problem, but here's a new problem that came up as a result of solving the original question, because I want to be able to award the bounty to someone!!!

Once I figured out my problem, I soon found out that when the ObservableCollection updates, the databound ComboBox has its contents repopulated, but most of the selections have been blanked out.

I assume that in this case, MVVM is going to make it difficult for me to remember the last selected item. I have an idea, but it seems a little nasty. I'll award the bounty to whomever comes up with a nice solution for this!

Question re-written on 2010-03-24

I have two UserControls, where one is a dialog that has a TabControl, and the other is one that appears within said TabControl. I'll just call them CandyDialog and CandyNameViewer for simplicity's sake. There's also a data management class called Tracker that manages information storage, which for all intents and purposes just exposes a public property that is an ObservableCollection.

I display the CandyNameViewer in CandyDialog via code behind, like this:

private void CandyDialog_Loaded( object sender, RoutedEventArgs e)
{
  _candyviewer = new CandyViewer();
  _candyviewer.DataContext = _tracker;
  candy_tab.Content = _candyviewer;
}

The CandyViewer's XAML looks like this (edited for kaxaml):

<Page
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"&gt;
    <Page.Resources>
        <DataTemplate x:Key="CandyItemTemplate">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="120"></ColumnDefinition>
                    <ColumnDefinition Width="150"></ColumnDefinition>
                </Grid.ColumnDefinitions>
                <TextBox Grid.Column="0" Text="{Binding CandyName}" Margin="3"></TextBox>
                <!-- just binding to DataContext ends up using InventoryItem as parent, so we need to get to the UserControl -->
                <ComboBox Grid.Column="1" SelectedItem="{Binding SelectedCandy, Mode=TwoWay}" ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}, Path=DataContext.CandyNames}" Margin="3"></ComboBox>
            </Grid>
        </DataTemplate>
    </Page.Resources>

    <Grid>
        <ListBox DockPanel.Dock="Top" ItemsSource="{Binding CandyBoxContents, Mode=TwoWay}" ItemTemplate="{StaticResource CandyItemTemplate}" />
    </Grid>
</Page>

Now everything works fine when the controls are loaded. As long as CandyNames is populated first, and then the consumer UserControl is displayed, all of the names are there. I obviously don't get any errors in the Output Window or anything like that.

The issue I have is that when the ObservableCollection is modified from the model, those changes are not reflected in the consumer UserControl! I've never had this problem before; all of my previous uses of ObservableCollection updated fine, although in those cases I wasn't databinding across assemblies. Although I am currently only adding and removing candy names to/from the ObservableCollection, at a later date I will likely also allow renaming from the model side.

Is there something I did wrong? Is there a good way to actually debug this? Reed Copsey indicates here that inter-UserControl databinding is possible. Unfortunately, my favorite Bea Stollnitz article on WPF databinding debugging doesn't suggest anything that I could use for this particular problem.

A: 

It's sounds like your object in the ObservableCollection don't want display changes (or ObservableCollection don't want change???)If first, I would be to use INotifyPropertyChanged interface in my object model.

Victor
INotifyPropertyChanged is great for properties, but collections require INotifyCollectionChanged, and ObservableCollection does this out of the box already.
Dave
A: 

I'm no Bea Stolnitz (regrettably, as she's awesome), but I wonder if it's possible that through the mysteries of expressing databinding relationships in XAML (which is NOT awesome in the slightest, IMO) the combobox is losing track of the fact that the property to which it is being bound is, in fact, an ObservableCollection. You might want to try exposing the ObservableCollection from, say, your datamodel, or some other layer that the combobox could bind to directly (rather than through the tab control). You might have to use a bit of C# to do this, rather than XAML, and it might offend some of the MVVM purists, but the many hours and days that I've spent troubleshooting problems like this have given me a bit of a distaste for XAML and MVVM. In my opinion, if I can't get a complicated databinding scenario working within an hour or so, it might be time to try a different approach.

Ken Smith
Thanks, Ken. I must have missed that detail, but even though the TabControl "hosts" my ChildWindow (i.e. tabcontrol.tabitem.content = childwindow), I am not trying to databind through it. I have set the DataContext of the ChildWindow specifically to the object that exposes the public ObservableCollection property...
Dave
You want to be a woman???
Daniel
I certainly want to know as much about databinding as she does :-). Not likely to happen anytime in the near future, however.
Ken Smith
@Ken Smith: While I value Bea Stolnitz's knowledge a lot I think the highly complex databinding heralded as WPF's strength is in fact absurdly overengineered.
Thorsten79
@Thorsten79. Whether over, under or just badly engineered, I agree that it's a PITA. http://blog.wouldbetheologian.com/2009/07/why-wpf-databinding-is-awful-technology.html
Ken Smith
A: 

Your xaml confuses me, but I think you need your TextBox to bind like this: {Binding SelectedCandy.CandyName}

Having an ObservableCollection tells WPF when items are added or removed, so if you remove or add CandyNames from your model you should see the combobox update accordingly. But if you change CandyName then WPF requires you to implement INotifyPropertyChanged so that you give WPF binding a heads up

so it's pretty simple (if I understand your model)

public class Candy : INotifyPropertyChanged
{
  string _CandyName;
  public string CandyName
  {
    get { return _CandyName;}
    set { _CandyName = value; OnPropertyChanged("CandyName");
  }

  public event PropertyChangedEventHandler OnPropertyChanged;

  void OnPropertyChanged(string prop)
  {
    if (PropertyChanged != null) PropertyChanged(this,new PropertyChangedEventArgs(prop));
  }
}
Jose
The textbox binds okay because I'm using an ItemTemplate, so it's implied that I'm operating on the selected candy already.I don't need to change the names *yet*, but that will likely happen soon. Since the ObservableCollection only contains strings, I wonder if it's more straightforward to force the OC itself to "tell" WPF that the *collection* has changed.
Dave
A: 

Well, now I feel stupid. My code was poorly thought out, and only today did I figure out what I had done wrong. I had set the DataContext twice, each to a different object, and for whatever reason, one of them had CandyNames defined as a List!!! Argh...

I think it definitely pays to diagram the bindings and relationships in UML so that it's easy to track this stuff down. MVVM + databindings can sure get confusing at times.

Dave
+1  A: 

I have had the same problem with ComboBoxes blanking out when their DataContext changes. I have a relatively simple solution that uses a class I wrote called "ComboBoxFixer". Once implemented, you can solve this problem by simply replacing this:

<ComboBox ItemsSource="..." SelectedItem="..." />

with this:

<ComboBox ItemsSource="..." my:ComboBoxFixer.SelectedItem="..." />

Explanation of the problem

The reason your ComboBoxes are coming up empty is that the SelectedItems binding is being evaluated when ItemsSource is not set. This can be fixed by delaying the transfer of data to and from SelectedItems until after all other data binding has completed.

How to implement ComboBoxFixer

My ComboBoxFixer class is implemented using a general dependency property synchronizer class I wrote. My DependencyPropertySynchronizer class has the following interface:

public class DependencyPropertySynchronizer
{
  public DispatcherPriority Priority { get; set; }
  public DependencyProperty AutoSyncProperty { get; set; }

  public DependencyProperty Register(...
  public DependencyProperty RegisterAttached(...
  public DependencyPropertyKey RegisterReadOnly(...
  public DependencyPropertyKey RegisterAttachedReadOnly(...
}

And is typically used like this:

public SomeClass : DependencyObject
{
  static DependencyPropertySynchronizer sync =
    new DependencyPropertySynchronizer
    {
      Priority = DispatcherPriority.ApplicationIdle
    };

  public static readonly DependencyProperty HappinessProperty =
   sync.RegisterAttached("Happiness", typeof(int), typeof(SomeClass));

  public static readonly DependencyProperty JoyProperty =
   sync.RegisterAttached("Joy", typeof(int), typeof(SomeClass));
}

The above code will cause the attached Happiness and Joy properties of any given object to remain synchronized: Whenever either Happiness or Joy is set, the other will be set at DispatcherPriority.ApplicationIdle. DependencyPropertySynchronizer is implemented using a hidden attached property which stores the last value set on either property and coordinates the scheduling of updates. You can also synchronize with an existing property by setting AutoSyncProperty.

Using this class, my ComboBoxFixer class is very simple:

public class ComboBoxFixer : DependencyObject
{
  static DependencyPropertySynchronizer sync =
    new DependencyPropertySynchronizer
    {
      Priority = DispatcherPriority.ApplicationIdle,
      AutoSyncProperty = Selector.SelectedItemProperty,
    };

  public static readonly DependencyProperty SelectedItemProperty =
    sync.RegisterAttached("SelectedItem", typeof(object), typeof(ComboBoxFixer),
    new FrameworkPropertyMetadata
    {
      BindsTwoWayByDefault = true,
    });

  public static object GetSelectedItem(...  // normal attached property stuff
  public static void SetSelectedItem(...
}

How it works

Whenever my:ComboBoxFixer.SelectedItem changes, the synchronizer updates the Selector.SelectedItem at ApplicationIdle priority, or vice-versa.

The data flow is:

ViewModel property
   <- bound from ->
      my:ComboBoxFixer.SelectedItem
         <- synced with ->
            ComboBox.SelectedItem

Additional note

In certain circumstances if you are actually switching the ItemsSource, it is possible for the SelectedItem to be set to null more recently than the correct value. This can be solved by adding a validation feature to DependencyObjectSynchronizer, then use it to ignore null values during synchronization.

Ray Burns