21
votes

I've got a simple problem in my WPF application which has me banging my head on the table. I have a TabControl, where every TabItem is a View generated for a ViewModel using a DataTemplate similar to this:

<DataTemplate DataType="{x:Type vm:FooViewModel}">
    <vw:FooView/>
</DataTemplate>

FooView contains a ComboBox:

<ComboBox ItemsSource="{Binding Path=BarList}" DisplayMemberPath="Name" SelectedItem="{Binding Path=SelectedBar}"/>

and FooViewModel contains a simple Property: public Bar SelectedBar { get; set; }. My problem is that when I set the value for my ComboBox, change to another tab, then change back, the ComboBox is empty again. If I set a breakpoint on the setter for my property, I see that the property is assigned to null when I switch to another tab.

From what I understand, when a tab is switched, it is removed from the VisualTree - but why is it setting my ViewModel's property to null? This is making it very difficult for me to hold persistent state, and checking value != null does not seem like the right solution. Can anyone shed some like on this situation?

Edit: The call stack at the setter breakpoint only shows [External Code] - no hints there.

11
Have you checked in code that the selecteditem is being set the first time? I've had a few cases where the selection is visible but selecteditem==null, especially using SubSonic 3 classes.SteveCav
That's a good thought - but the value is definitely stored the first time. When I break, I can see that value = null and my variable is storing the previously selected value.bsg
Can you show call stack for that breakpoint?Fyodor Soikin
No - I should have mentioned that, but the only information on the call stack is the current call setting the property to null, and [External Code].bsg
Do you think this has got something to do with an event that is raised when you switch tabs? I mean, the event could tunnel down to the ComboBox that triggers the change of the SelectedItem?Michael Detras

11 Answers

25
votes

we just ran into the same problem. We found a blog entry describing the problem. It looks like it is a bug in WPF and there is a workaround: Specify the SelectedItem binding before the ItemsSource binding and the problem should be gone.

The link to the blog article:

http://www.metanous.be/pharcyde/post/Bug-in-WPF-combobox-databinding.aspx

3
votes

My app is using avalondock & prims and had that exact problem. I has same thought with BSG, when we switched tab or document content in MVVM app, the controls as listview+box, combobox is removed from VisualTree. I bugged and saw most data of them was reset to null such as itemssource, selecteditem, .. but selectedboxitem was still hold current value.

A approach is in model, check its value is null then return like this:

 private Employee _selectedEmployee;
 public Employee SelectedEmployee
 {
     get { return _selectedEmployee; }
     set
     {
        if (_selectedEmployee == value || 
            IsAdding ||
            (value == null && Employees.Count > 0))
    {
        return;
    }

    _selectedEmployee = value;
    OnPropertyChanged(() => SelectedEmployee);
} 

But this approach can only solve quite good in first binding level. i mean, how we go if want to bind SelectedEmployee.Office to combobox, do same is not good if check in propertyChanged event of SelectedEmployee model.

Basically, we dont want its value is reset null, keep its pre-value. I found a new solution consistently. By using attached property, i created KeepSelection a-Pro, bool type, for Selector controls, thus supply all its inherited suck as listview, combobox...

public class SelectorBehavior
{

public static bool GetKeepSelection(DependencyObject obj)
{
    return (bool)obj.GetValue(KeepSelectionProperty);
}

public static void SetKeepSelection(DependencyObject obj, bool value)
{
    obj.SetValue(KeepSelectionProperty, value);
}

// Using a DependencyProperty as the backing store for KeepSelection.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty KeepSelectionProperty =
    DependencyProperty.RegisterAttached("KeepSelection", typeof(bool), typeof(SelectorBehavior), 
    new UIPropertyMetadata(false,  new PropertyChangedCallback(onKeepSelectionChanged)));

static void onKeepSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var selector = d as Selector;
    var value = (bool)e.NewValue;
    if (value)
    {
        selector.SelectionChanged += selector_SelectionChanged;
    }
    else
    {
        selector.SelectionChanged -= selector_SelectionChanged;
    }
}

static void selector_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var selector = sender as Selector;

    if (e.RemovedItems.Count > 0)
    {
        var deselectedItem = e.RemovedItems[0];
        if (selector.SelectedItem == null)
        {
            selector.SelectedItem = deselectedItem;
            e.Handled = true;
        }
    }
}
}

Final, i use this approach simply in xaml:

<ComboBox lsControl:SelectorBehavior.KeepSelection="true"  
        ItemsSource="{Binding Offices}" 
        SelectedItem="{Binding SelectedEmployee.Office}" 
        SelectedValuePath="Id" 
        DisplayMemberPath="Name"></ComboBox>

But, selecteditem will never null if selector's itemssource has items. It may affect some special context.

Hope that helps. Happy conding! :D

longsam

1
votes

Generally, I use SelectedValue instead of SelectedItem. If I need the object associated with the SelectedValue then I add a lookup field containing this to the target object (as I use T4 templates to gen my viewmodels this tends to be in a partial class). If you use a nullable property to store the SelectedValue then you will experience the problem described above, however if binding the SelectedValue to a non-nullable value (such as an int) then the WPF binding engine will discard the null value as being inappropriate for the target.

1
votes

Edit: Below stuff works (I hope...); I developed it because I followed the SelectedItems route described on the MVVM Lite page. However - why do I want to rely on SelectedItems? Adding an IsSelected property to my Items (as shown here) automatically preserves selected items (short of the mentioned cavet in above link). In the end, much easier!

Inital Post: ok - that was a piece of work; I've a multi-column ListView with SelectionMode="Extension", which makes the whole thing fairly complex. My starting point is invoking tabItems from workspaces similar as describe here.

  1. I made sure that in my ViewModel, I know when a tab item (workspace) is active. (This is a bit similar to here) - of course, somebody needs initalize SelectedWorkspace first.

    private Int32 _selectedWorkspace;
    public Int32 SelectedWorkspace {
      get { return _selectedWorkspace; }
      set {
        _selectedWorkspace = value;
        base.OnPropertyChanged("SelectedWorkspace");
      }
    }
    protected Int32 _thisWorkspaceIdx = -1;
    protected Int32 _oldSelectedWorkspace = -1;
    public void OnSelectedWorkspaceChanged(object sender, PropertyChangedEventArgs e) {
      if (e.PropertyName == "SelectedWorkspace") {
        if (_oldSelectedWorkspace >= 0) {
          Workspaces[_oldSelectedWorkpace].OnIsActivatedChanged(false);
        }
        Workspaces[SelectedWorkspace].OnIsActivatedChanged(true);
        _oldSelectedWorkspace = SelectedWorkspace;
      }
    }
    protected bool _isActive = false;
    protected virtual void OnIsActivatedChanged(bool isActive) {
      _isActive = isActive;
    }
    
  2. This allowed me to update the ViewModel selected items only if the tab item (workspace) was actually active. Hence, my ViewModel selected items list is preserved even as the tab item clears the ListView.SelectedItems. In the ViewModel:

    if (_isActive) { 
      // ... update ViewModel selected items, referred below as vm.selectedItems
    }
    
  3. Last, when the tabItem got re-enabled, I hooked up to the 'Loaded' event and restored the SelectedItems. This is done in the code-behind of the View. (Note that whilst my ListView has multiple columns, one serves as a key, the others are for information only. the ViewModel selectedItems list only keeps the key. Else, the comparison below would be more complex):

    private void myList_Loaded(object sender, RoutedEventArgs e) {
      myViewModel vm = DataContext as myViewModel;
      if (vm.selectedItems.Count > 0) {
        foreach (string myKey in vm.selectedItems) {
          foreach (var item in myList.Items) {
            MyViewModel.MyItem i = item as MyViewModel.MyItem;
            if (i.Key == myKey) {
              myList.SelectedItems.Add(item);
            }
          }
        }
      }
    }
    
1
votes

if you suing async selection in WPF then remove it IsSynchronizedWithCurrentItem="True" from for the ComboBox, please refer to the document about IsSynchronizedWithCurrentItem:

<ComboBox 
    Name="tmpName" 
    Grid.Row="10" 
    Width="250" 
    Text="Best Match Position List" 
    HorizontalAlignment="Left" 
    Margin="14,0,0,0"

    SelectedItem="{Binding Path=selectedSurceList,Mode=TwoWay}"
    ItemsSource="{Binding Path=abcList}"  
    DisplayMemberPath="Name"
    SelectedValuePath="Code"
    IsEnabled="{Binding ElementName=UserBestMatchYesRadioBtn,Path=IsChecked}">
</ComboBox>

also takecare the binding first use SelectedItem then ItemsSource

ref: http://social.msdn.microsoft.com/Forums/vstudio/en-US/fb8a8ad2-83c1-43df-b3c9-61353979d3d7/comboboxselectedvalue-is-lost-when-itemssource-is-updated?forum=wpf

http://social.msdn.microsoft.com/Forums/en-US/c9e62ad7-926e-4612-8b0c-cc75fbd160fd/bug-in-wpf-combobox-data-binding

I solve my problem using the above

0
votes

I once had a similar problem. It seems that the combobox looses the selected item in VisibilityChanged event. Workarround is to clear the binding before this occurs, and reset it when coming back. You can also try to set the Binding to Mode=TwoWay

Hope that this helps

Jan

0
votes

I had the same problem and solved it with the following method attached to the Combobox DataContextChanged-Event:

private void myCombobox_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    if (sender is FrameworkElement && e.NewValue == null)
        ((FrameworkElement)sender).DataContext = e.OldValue;
}

So everytime you want to remove the datacontext from the combobox, the old datacontext will be set again.

Everytime you change the active Tab of your TabControl, the Combobox will removed from your VisualTree and added if you go back to the one with your combobox. If the combo box is removed from the VisualTree, also the DataContext is set to null.

Or you use a class, which have implemented such feature:

public class MyCombobox : ComboBox
{
    public MyCombobox()
    {
        this.DataContextChanged += MyCombobox_DataContextChanged;
    }

    void MyCombobox_DataContextChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
    {
        if (sender is FrameworkElement && e.NewValue == null)
            ((FrameworkElement)sender).DataContext = e.OldValue;
    }
    public void SetDataContextExplicit(object dataContext)
    {
        lock(this.DataContext)
        {
            this.DataContextChanged -= MyCombobox_DataContextChanged;
            this.DataContext = dataContext;
            this.DataContextChanged += MyCombobox_DataContextChanged;
        }
    }
}
0
votes

I think the problem may be that you arent telling the Combo box when to bind back to the source. Try this:

<ComboBox ItemsSource="{Binding Path=BarList}" DisplayMemberPath="Name" SelectedItem="{Binding Path=SelectedBar,  UpdateSourceTrigger=PropertyChanged}"/
0
votes

I had this same problem when scrolling through a virtualizing DataGrid that contains ComboBoxes. Using IsSynchronizedWithCurrentItem did not work, nor did changing the order of the SelectedItem and ItemsSource bindings. But here is an ugly hack that seems to work:

First, give your ComboBox an x:Name. This should be in the XAML for a control with a single ComboBox. For example:

<ComboBox x:Name="mComboBox" SelectedItem="{Binding SelectedTarget.WritableData, Mode=TwoWay}">

Then add these two event handlers in your codebehind:

using System.Windows.Controls;
using System.Windows;

namespace SATS.FileParsing.UserLogic
{
    public partial class VariableTargetSelector : UserControl
    {
        public VariableTargetSelector()
        {
            InitializeComponent();
            mComboBox.DataContextChanged += mComboBox_DataContextChanged;
            mComboBox.SelectionChanged += mComboBox_SelectionChanged;
        }

        /// <summary>
        /// Without this, if you grab the scrollbar and frantically scroll around, some ComboBoxes get their SelectedItem set to null.
        /// Don't ask me why.
        /// </summary>
        void mComboBox_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            mComboBox.GetBindingExpression(ComboBox.SelectedItemProperty).UpdateTarget();
        }

        /// <summary>
        /// Without this, picking a new item in the dropdown does not update IVariablePair.SelectedTarget.WritableData.
        /// Don't ask me why.
        /// </summary>
        void mComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            mComboBox.GetBindingExpression(ComboBox.SelectedItemProperty).UpdateSource();
        }
    }
}
0
votes

You can use the MVVM framework Catel and the catel:TabControl element there this problem is already solved.

0
votes

Just don't allow your ViewModel's property to be changed if value becomes null.

public Bar SelectedBar
{
    get { return barSelected; }
    set { if (value != null) SetProperty(ref barSelected, value); }
}

That's it.